Source/Charts/Renderers/ScatterChartRenderer.swift (167 lines of code) (raw):
//
// ScatterChartRenderer.swift
// Charts
//
// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda
// A port of MPAndroidChart for iOS
// Licensed under Apache License 2.0
//
// https://github.com/danielgindi/Charts
//
import Foundation
import CoreGraphics
open class ScatterChartRenderer: LineScatterCandleRadarRenderer
{
@objc open weak var dataProvider: ScatterChartDataProvider?
@objc public init(dataProvider: ScatterChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler)
{
super.init(animator: animator, viewPortHandler: viewPortHandler)
self.dataProvider = dataProvider
}
open override func drawData(context: CGContext)
{
guard let scatterData = dataProvider?.scatterData else { return }
// If we redraw the data, remove and repopulate accessible elements to update label values and frames
accessibleChartElements.removeAll()
if let chart = dataProvider as? ScatterChartView {
// Make the chart header the first element in the accessible elements array
let element = createAccessibleHeader(usingChart: chart,
andData: scatterData,
withDefaultDescription: "Scatter Chart")
accessibleChartElements.append(element)
}
// TODO: Due to the potential complexity of data presented in Scatter charts, a more usable way
// for VO accessibility would be to use axis based traversal rather than by dataset.
// Hence, accessibleChartElements is not populated below. (Individual renderers guard against dataSource being their respective views)
let sets = scatterData.dataSets as? [ScatterChartDataSet]
assert(sets != nil, "Datasets for ScatterChartRenderer must conform to IScatterChartDataSet")
let drawDataSet = { self.drawDataSet(context: context, dataSet: $0) }
sets!.lazy
.filter(\.isVisible)
.forEach(drawDataSet)
}
private var _lineSegments = [CGPoint](repeating: CGPoint(), count: 2)
@objc open func drawDataSet(context: CGContext, dataSet: ScatterChartDataSetProtocol)
{
guard let dataProvider = dataProvider else { return }
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency)
let phaseY = animator.phaseY
let entryCount = dataSet.entryCount
var point = CGPoint()
let valueToPixelMatrix = trans.valueToPixelMatrix
if let renderer = dataSet.shapeRenderer
{
context.saveGState()
for j in 0 ..< Int(min(ceil(Double(entryCount) * animator.phaseX), Double(entryCount)))
{
guard let e = dataSet.entryForIndex(j) else { continue }
point.x = CGFloat(e.x)
point.y = CGFloat(e.y * phaseY)
point = point.applying(valueToPixelMatrix)
if !viewPortHandler.isInBoundsRight(point.x)
{
break
}
if !viewPortHandler.isInBoundsLeft(point.x) ||
!viewPortHandler.isInBoundsY(point.y)
{
continue
}
renderer.renderShape(context: context, dataSet: dataSet, viewPortHandler: viewPortHandler, point: point, color: dataSet.color(atIndex: j))
}
context.restoreGState()
}
else
{
print("There's no ShapeRenderer specified for ScatterDataSet", terminator: "\n")
}
}
open override func drawValues(context: CGContext)
{
guard
let dataProvider = dataProvider,
let scatterData = dataProvider.scatterData
else { return }
// if values are drawn
if isDrawingValuesAllowed(dataProvider: dataProvider)
{
let phaseY = animator.phaseY
var pt = CGPoint()
for i in scatterData.indices
{
guard let dataSet = scatterData[i] as? ScatterChartDataSetProtocol,
shouldDrawValues(forDataSet: dataSet)
else { continue }
let valueFont = dataSet.valueFont
let formatter = dataSet.valueFormatter
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency)
let valueToPixelMatrix = trans.valueToPixelMatrix
let iconsOffset = dataSet.iconsOffset
let angleRadians = dataSet.valueLabelAngle.DEG2RAD
let shapeSize = dataSet.scatterShapeSize
let lineHeight = valueFont.lineHeight
_xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator)
for j in _xBounds
{
guard let e = dataSet.entryForIndex(j) else { break }
pt.x = CGFloat(e.x)
pt.y = CGFloat(e.y * phaseY)
pt = pt.applying(valueToPixelMatrix)
if (!viewPortHandler.isInBoundsRight(pt.x))
{
break
}
// make sure the lines don't do shitty things outside bounds
if (!viewPortHandler.isInBoundsLeft(pt.x)
|| !viewPortHandler.isInBoundsY(pt.y))
{
continue
}
let text = formatter.stringForValue(
e.y,
entry: e,
dataSetIndex: i,
viewPortHandler: viewPortHandler)
if dataSet.isDrawValuesEnabled
{
context.drawText(text,
at: CGPoint(x: pt.x,
y: pt.y - shapeSize - lineHeight),
align: .center,
angleRadians: angleRadians,
attributes: [.font: valueFont,
.foregroundColor: dataSet.valueTextColorAt(j)]
)
}
if let icon = e.icon, dataSet.isDrawIconsEnabled
{
context.drawImage(icon,
atCenter: CGPoint(x: pt.x + iconsOffset.x,
y: pt.y + iconsOffset.y),
size: icon.size)
}
}
}
}
}
open override func drawExtras(context: CGContext)
{
}
open override func drawHighlighted(context: CGContext, indices: [Highlight])
{
guard
let dataProvider = dataProvider,
let scatterData = dataProvider.scatterData
else { return }
context.saveGState()
for high in indices
{
guard
let set = scatterData[high.dataSetIndex] as? ScatterChartDataSetProtocol,
set.isHighlightEnabled
else { continue }
guard let entry = set.entryForXValue(high.x, closestToY: high.y) else { continue }
if !isInBoundsX(entry: entry, dataSet: set) { continue }
context.setStrokeColor(set.highlightColor.cgColor)
context.setLineWidth(set.highlightLineWidth)
if set.highlightLineDashLengths != nil
{
context.setLineDash(phase: set.highlightLineDashPhase, lengths: set.highlightLineDashLengths!)
}
else
{
context.setLineDash(phase: 0.0, lengths: [])
}
let x = entry.x // get the x-position
let y = entry.y * Double(animator.phaseY)
let trans = dataProvider.getTransformer(forAxis: set.axisDependency)
let pt = trans.pixelForValues(x: x, y: y)
high.setDraw(pt: pt)
// draw the lines
drawHighlightLines(context: context, point: pt, set: set)
}
context.restoreGState()
}
}