Code to draw a graph of data. Each abscissa has an ordinate range to be displayed as a line segment. All data, i.e., scaled points are verified to be within the declared analysisView.bounds. strokeColors are verified within the range 0..1
BTW, no I don't need animation for this static data, but CALayer seemed to require more coding, and I found fewer code examples for it.
The code below has two problems:
1) it doesn't draw into the window
- the weird behavior of min/max
The first is why I am posting. What am I missing?
import AppKit
class AnalysisViewController: NSViewController {
@IBOutlet var analysisView: NSView!
var ranges = [ClosedRange<Double>]()
var ordinateMinimum = CGFloat()
var ordinateMaximum = CGFloat()
var ordinateScale = CGFloat()
let abscissaMinimum:CGFloat = 1
let abscissaMaximum:CGFloat = 92
let abscissaScale :CGFloat = 800/92
let shapeLayer = CAShapeLayer()
var points = [CGPoint]() // created just to verify (in debugger area) that points are within analysisView.bounds
func genrateGraph() {
// ranges.append(0...0) // inexplicably FAILS! @ ordinateMinimum/ordinateMaximum if replaces "if N == 1" below
// ranges.append(0.1...0.1) // non-zero range does not fail but becomes the min or max, therefore, not useful
for N in 1...92 {
if let element = loadFromJSON(N) {
if N == 1 { ranges.append( element.someFunction() ) } // ranges[0] is an unused placeholder
// if N == 1 { ranges.append(0...0) } // inexplicably FAILS! @ ordinateMinimum/ordinateMaximum if replacing above line
ranges.append( element.someFunction() )
}
else { ranges.append(0...0) } // some elements have no range data
}
ordinateMinimum = CGFloat(ranges.min(by: {$0 != 0...0 && $1 != 0...0 && $0.lowerBound < $1.lowerBound})!.lowerBound)
ordinateMaximum = CGFloat(ranges.max(by: {$0 != 0...0 && $1 != 0...0 && $0.upperBound < $1.upperBound})!.upperBound)
ordinateScale = analysisView.frame.height/(ordinateMaximum - ordinateMinimum)
for range in 1..<ranges.count {
shapeLayer.addSublayer(CALayer()) // sublayer each abscissa range so that .strokeColor can be assigned to each
// shapeLayer.frame = CGRect(x: 0, y: 0, width: analysisView.frame.width, height: analysisView.frame.height) // might be unneccessary
let path = CGMutablePath() // a new path for every sublayer, i.e., range that is displayed as line segment
points.append(CGPoint(x: CGFloat(range)*abscissaScale, y: CGFloat(ranges[range].lowerBound)*ordinateScale))
path.move(to: points.last! )
points.append(CGPoint(x: CGFloat(range)*abscissaScale, y: CGFloat(ranges[range].upperBound)*ordinateScale))
path.addLine(to: points.last! )
path.closeSubpath()
shapeLayer.path = path
// shapeLayer.strokeColor = CGColor.white
let r:CGFloat = 1.0/CGFloat(range)
let g:CGFloat = 0.3/CGFloat(range)
let b:CGFloat = 0.7/CGFloat(range)
// print("range: \(range)\tr: \(r)\tg: \(g)\tb: \(b)") // just to verify 0...1 values
shapeLayer.strokeColor = CGColor(srgbRed: r, green: g, blue: b, alpha: 1.0)
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.wantsLayer = true // one of these (view or analysisView) must be unneccessary
view.frame = CGRect(x: 0, y: 0, width: 840, height: 640)
analysisView.wantsLayer = true
analysisView.frame = CGRect(x: 0, y: 0, width: 840, height: 640)
genrateGraph()
}
}
Your code contains something unclear: loadFromJSON
, someFunction
and the type having the method someFunction
.
And there is no sample data.
So, we readers cannot test it and hard to say something sure.
But some parts of your code are clearly bad to use CAShapeLayer
.
You create only one CAShapeLayer
with this line let shapeLayer = CAShapeLayer()
,
but shapeLayer
is not added as sublayer to any other layer in your code.
And, if you want multiple shape layers having different color each, you need to create multiple shape layers.
As already written, your code has many missing parts, so I have written a simplified example.
Please try this:
import AppKit
class AnalysisViewController: NSViewController {
@IBOutlet var analysisView: NSView!
func genrateGraph() {
for bar: (CGFloat, CGFloat, CGFloat, ClosedRange<CGFloat>) in [
(1.0, 0.0, 0.0, 0...100),
(0.0, 1.0, 0.0, 100...200),
(1.0, 1.0, 0.0, 200...300),
(0.0, 0.0, 1.0, 300...400),
(1.0, 0.0, 1.0, 400...500),
(0.0, 1.0, 1.0, 500...600),
(1.0, 1.0, 1.0, 600...700),
] {
let color = CGColor(srgbRed: bar.0, green: bar.1, blue: bar.2, alpha: 1.0)
let x = bar.3.lowerBound
let width = bar.3.upperBound - bar.3.lowerBound
//
//Create `CAShapeLayer` for each color
let layer = CAShapeLayer()
analysisView.layer?.addSublayer(layer)
let height: CGFloat = 320
let rect = CGRect(x: x, y: 0, width: width, height: height)
print(rect)
let path = CGMutablePath()
path.addRect(rect)
layer.path = path
layer.fillColor = color
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.frame = CGRect(x: 0, y: 0, width: 840, height: 640)
analysisView.wantsLayer = true
analysisView.frame = CGRect(x: 0, y: 0, width: 840, height: 640)
genrateGraph()
}
}
If this code does show you a grey window, you need to check if you properly setup AnalysisViewController
on your storyboard.
By the way,
- To work with
CAShapeLayer
, usingCGPath
(includingCGMutablePath
) is the simpler way. CGPath
(orCGMutablePath
) does not have a methodstroke()
and you have no need to call it. (In fact, you cannot!)