Skip to content

Instantly share code, notes, and snippets.

@PedroCavaleiro
Created May 25, 2021 16:49
Show Gist options
  • Save PedroCavaleiro/5dad7ba55bcf1498089f177a08f5e5e6 to your computer and use it in GitHub Desktop.
Save PedroCavaleiro/5dad7ba55bcf1498089f177a08f5e5e6 to your computer and use it in GitHub Desktop.
Multi color line (Charts Framework for Swift)
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