Created
May 25, 2021 16:49
-
-
Save PedroCavaleiro/5dad7ba55bcf1498089f177a08f5e5e6 to your computer and use it in GitHub Desktop.
Multi color line (Charts Framework for Swift)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ColoredLineChartRenderer: LineChartRenderer { | |
// | |
// var chartHeight:CGFloat = 0 | |
private var myXBounds = BarLineScatterCandleBubbleRenderer.XBounds() | |
// added to support the commented "Color Section" code below | |
// min & maximum visible data geometry coordinate | |
private var minVisiblePoint = CGPoint.zero | |
private var maxVisiblePoint = CGPoint.zero | |
init(view: LineChartView) { | |
super.init(dataProvider: view, animator: view.chartAnimator, viewPortHandler: view.viewPortHandler) | |
} | |
private struct ColorSection { | |
// section of graph with specific color | |
var min: CGFloat // In data geometry | |
var max: CGFloat // In data geometry | |
var strokeColor: NSColor | |
// var fillColor: UIColor { return strokeColor.withAlphaComponent(0.2) } | |
static func topBottom(min: Double, max: Double) -> [ColorSection] { | |
if SettingsHelper.shared.chart.showWarningArea { | |
return [ColorSection(min: CGFloat(SettingsHelper.shared.chart.warningAreaLowerBound), | |
max: CGFloat(min - 1), | |
strokeColor: NSColor(cgColor: CGColor(red: 255/255.0, green: 50/255.0, blue: 00/255.0, alpha: 0.8))!), | |
ColorSection(min: CGFloat(SettingsHelper.shared.chart.warningAreaUpperBound), | |
max: CGFloat(max + 1), | |
strokeColor: NSColor(cgColor: CGColor(red: 126/255.0, green: 211/255.0, blue: 33/255.0, alpha: 0.8))!), | |
ColorSection(min: CGFloat(SettingsHelper.shared.chart.warningAreaLowerBound), | |
max: CGFloat(SettingsHelper.shared.chart.warningAreaUpperBound), | |
strokeColor: NSColor(cgColor: CGColor(red: 248/255.0, green: 231/255.0, blue: 28/255.0, alpha: 0.8))!) | |
] | |
} else { | |
return [ColorSection(min: 0, | |
max: CGFloat(min - 1), | |
strokeColor: NSColor(cgColor: CGColor(red: 255/255.0, green: 50/255.0, blue: 00/255.0, alpha: 0.8))!), | |
ColorSection(min: 0, | |
max: CGFloat(max + 1), | |
strokeColor: NSColor(cgColor: CGColor(red: 126/255.0, green: 211/255.0, blue: 33/255.0, alpha: 0.8))!) | |
] | |
} | |
} | |
} | |
@objc override func drawLinear(context: CGContext, dataSet: ILineChartDataSet) { | |
guard let dataProvider = dataProvider else { return } | |
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) | |
let valueToPixelMatrix = trans.valueToPixelMatrix | |
let entryCount = dataSet.entryCount | |
let isDrawSteppedEnabled = dataSet.mode == .stepped | |
// let pointsPerEntryPair = isDrawSteppedEnabled ? 4 : 2 | |
let phaseY = animator.phaseY | |
myXBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) | |
if dataSet.isDrawFilledEnabled && entryCount > 0 | |
{ | |
drawLinearFill(context: context, dataSet: dataSet, trans: trans, bounds: myXBounds) | |
} | |
context.saveGState() | |
defer { context.restoreGState() } | |
// only one color per dataset | |
guard dataSet.entryForIndex(myXBounds.min) != nil else { | |
return | |
} | |
var firstPoint = true | |
let path = CGMutablePath() | |
for x in stride(from: myXBounds.min, through: myXBounds.range + myXBounds.min, by: 1) | |
{ | |
guard let e1 = dataSet.entryForIndex(x == 0 ? 0 : (x - 1)) else { continue } | |
guard let e2 = dataSet.entryForIndex(x) else { continue } | |
let startPoint = | |
CGPoint( | |
x: CGFloat(e1.x), | |
y: CGFloat(e1.y * phaseY)) | |
.applying(valueToPixelMatrix) | |
if firstPoint | |
{ | |
path.move(to: startPoint) | |
firstPoint = false | |
} | |
else | |
{ | |
path.addLine(to: startPoint) | |
} | |
if isDrawSteppedEnabled | |
{ | |
let steppedPoint = | |
CGPoint( | |
x: CGFloat(e2.x), | |
y: CGFloat(e1.y * phaseY)) | |
.applying(valueToPixelMatrix) | |
path.addLine(to: steppedPoint) | |
} | |
let endPoint = | |
CGPoint( | |
x: CGFloat(e2.x), | |
y: CGFloat(e2.y * phaseY)) | |
.applying(valueToPixelMatrix) | |
path.addLine(to: endPoint) | |
} | |
let graphSize = CGSize(width: viewPortHandler.chartWidth, height: viewPortHandler.chartWidth) | |
for band in ColorSection.topBottom(min: dataSet.yMin, max: dataSet.yMax) { | |
let y0 = max(CGPoint(x: 0, y: band.min).applying(valueToPixelMatrix).y, 0) | |
let y1 = min(CGPoint(x: 0, y: band.max).applying(valueToPixelMatrix).y, graphSize.height) | |
context.saveGState() | |
context.clip(to: CGRect(x: 0, y: y0, width: graphSize.width, height: y1 - y0)) | |
band.strokeColor.setStroke() | |
context.setLineWidth(2) | |
context.addPath(path) | |
context.strokePath() | |
context.restoreGState() | |
} | |
} | |
@objc override func drawHorizontalBezier(context gc: CGContext, dataSet: ILineChartDataSet) { | |
guard let dataProvider = dataProvider else { return } | |
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) | |
let phaseY = animator.phaseY | |
myXBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) | |
// the path for the cubic-spline | |
let cubicDrawPath = CGMutablePath() | |
let valueToPixelMatrix = trans.valueToPixelMatrix | |
gc.saveGState() | |
gc.beginPath() | |
if myXBounds.range >= 1 { | |
var prev: ChartDataEntry! = dataSet.entryForIndex(myXBounds.min) | |
var cur: ChartDataEntry! = prev | |
if cur == nil { return } | |
// let the spline start at zero | |
cubicDrawPath.move(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(cur.y * phaseY)), transform: valueToPixelMatrix) | |
for j in myXBounds.dropFirst(1) { | |
prev = cur | |
cur = dataSet.entryForIndex(j) | |
// print("y: (cur.y) when x is (cur.x)") | |
// control point for curve | |
let cpx = CGFloat(prev.x + (cur.x - prev.x) / 2.0) | |
cubicDrawPath.addCurve( | |
to: CGPoint( | |
x: CGFloat(cur.x), | |
y: CGFloat(cur.y * phaseY)), | |
control1: CGPoint( | |
x: cpx, | |
y: CGFloat(prev.y * phaseY)), | |
control2: CGPoint( | |
x: cpx, | |
y: CGFloat(cur.y * phaseY)), | |
transform: valueToPixelMatrix) | |
} | |
let graphSize = CGSize(width: viewPortHandler.chartWidth, height: viewPortHandler.chartWidth) | |
for band in ColorSection.topBottom(min: dataSet.yMin, max: dataSet.yMax) { | |
let y0 = max(CGPoint(x: 0, y: band.min).applying(valueToPixelMatrix).y, 0) | |
let y1 = min(CGPoint(x: 0, y: band.max).applying(valueToPixelMatrix).y, graphSize.height) | |
gc.saveGState() // ; do { | |
gc.clip(to: CGRect(x: 0, y: y0, width: graphSize.width, height: y1 - y0)) | |
band.strokeColor.setStroke() | |
gc.setLineWidth(2) | |
gc.addPath(cubicDrawPath) | |
gc.strokePath() | |
gc.restoreGState() | |
} | |
} | |
gc.restoreGState() | |
} | |
@objc override func drawCubicBezier(context: CGContext, dataSet: ILineChartDataSet) { | |
guard let dataProvider = dataProvider else { return } | |
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) | |
let phaseY = animator.phaseY | |
myXBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) | |
let intensity = dataSet.cubicIntensity | |
// the path for the cubic-spline | |
let cubicPath = CGMutablePath() | |
let valueToPixelMatrix = trans.valueToPixelMatrix | |
if myXBounds.range >= 1 | |
{ | |
var prevDx: CGFloat = 0.0 | |
var prevDy: CGFloat = 0.0 | |
var curDx: CGFloat = 0.0 | |
var curDy: CGFloat = 0.0 | |
// Take an extra point from the left, and an extra from the right. | |
// That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. | |
// So in the starting `prev` and `cur`, go -2, -1 | |
let firstIndex = myXBounds.min + 1 | |
var prevPrev: ChartDataEntry! = nil | |
var prev: ChartDataEntry! = dataSet.entryForIndex(max(firstIndex - 2, 0)) | |
var cur: ChartDataEntry! = dataSet.entryForIndex(max(firstIndex - 1, 0)) | |
var next: ChartDataEntry! = cur | |
var nextIndex: Int = -1 | |
if cur == nil { return } | |
// let the spline start | |
cubicPath.move(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(cur.y * phaseY)), transform: valueToPixelMatrix) | |
for j in myXBounds.dropFirst() { | |
prevPrev = prev | |
prev = cur | |
cur = nextIndex == j ? next : dataSet.entryForIndex(j) | |
nextIndex = j + 1 < dataSet.entryCount ? j + 1 : j | |
next = dataSet.entryForIndex(nextIndex) | |
if next == nil { break } | |
prevDx = CGFloat(cur.x - prevPrev.x) * intensity | |
prevDy = CGFloat(cur.y - prevPrev.y) * intensity | |
curDx = CGFloat(next.x - prev.x) * intensity | |
curDy = CGFloat(next.y - prev.y) * intensity | |
cubicPath.addCurve( | |
to: CGPoint( | |
x: CGFloat(cur.x), | |
y: CGFloat(cur.y) * CGFloat(phaseY)), | |
control1: CGPoint( | |
x: CGFloat(prev.x) + prevDx, | |
y: (CGFloat(prev.y) + prevDy) * CGFloat(phaseY)), | |
control2: CGPoint( | |
x: CGFloat(cur.x) - curDx, | |
y: (CGFloat(cur.y) - curDy) * CGFloat(phaseY)), | |
transform: valueToPixelMatrix) | |
} | |
let graphSize = CGSize(width: viewPortHandler.chartWidth, height: viewPortHandler.chartWidth) | |
for band in ColorSection.topBottom(min: dataSet.yMin, max: dataSet.yMax) { | |
let y0 = max(CGPoint(x: 0, y: band.min).applying(valueToPixelMatrix).y, 0) | |
let y1 = min(CGPoint(x: 0, y: band.max).applying(valueToPixelMatrix).y, graphSize.height) | |
context.saveGState() // ; do { | |
context.clip(to: CGRect(x: 0, y: y0, width: graphSize.width, height: y1 - y0)) | |
band.strokeColor.setStroke() | |
context.setLineWidth(2) | |
context.addPath(cubicPath) | |
context.strokePath() | |
context.restoreGState() | |
} | |
} | |
context.saveGState() | |
defer { context.restoreGState() } | |
if dataSet.isDrawFilledEnabled | |
{ | |
// Copy this path because we make changes to it | |
let fillPath = cubicPath.mutableCopy() | |
drawCubicFill(context: context, dataSet: dataSet, spline: fillPath!, matrix: valueToPixelMatrix, bounds: myXBounds) | |
} | |
} | |
private func drawLine( | |
context: CGContext, | |
spline: CGMutablePath, | |
drawingColor: NSUIColor) { | |
context.beginPath() | |
context.addPath(spline) | |
context.setStrokeColor(drawingColor.cgColor) | |
context.strokePath() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment