Last active
January 31, 2019 09:16
-
-
Save willard1218/da8fa6b734feace543f9b718f809e17f to your computer and use it in GitHub Desktop.
demo for Vahe Kocharyan
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
// | |
// LineChartRenderer.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 | |
#if !os(OSX) | |
import UIKit | |
#endif | |
open class LineChartRenderer: LineRadarRenderer | |
{ | |
// TODO: Currently, this nesting isn't necessary for LineCharts. However, it will make it much easier to add a custom rotor | |
// that navigates between datasets. | |
// NOTE: Unlike the other renderers, LineChartRenderer populates accessibleChartElements in drawCircles due to the nature of its drawing options. | |
/// A nested array of elements ordered logically (i.e not in visual/drawing order) for use with VoiceOver. | |
private lazy var accessibilityOrderedElements: [[NSUIAccessibilityElement]] = accessibilityCreateEmptyOrderedElements() | |
@objc open weak var dataProvider: LineChartDataProvider? | |
@objc public init(dataProvider: LineChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler) | |
{ | |
super.init(animator: animator, viewPortHandler: viewPortHandler) | |
self.dataProvider = dataProvider | |
} | |
open override func drawData(context: CGContext) | |
{ | |
guard let lineData = dataProvider?.lineData else { return } | |
for i in 0 ..< lineData.dataSetCount | |
{ | |
guard let set = lineData.getDataSetByIndex(i) else { continue } | |
if set.isVisible | |
{ | |
if !(set is ILineChartDataSet) | |
{ | |
fatalError("Datasets for LineChartRenderer must conform to ILineChartDataSet") | |
} | |
drawDataSet(context: context, dataSet: set as! ILineChartDataSet) | |
} | |
} | |
} | |
@objc open func drawDataSet(context: CGContext, dataSet: ILineChartDataSet) | |
{ | |
if dataSet.entryCount < 1 | |
{ | |
return | |
} | |
context.saveGState() | |
context.setLineWidth(dataSet.lineWidth) | |
if dataSet.lineDashLengths != nil | |
{ | |
context.setLineDash(phase: dataSet.lineDashPhase, lengths: dataSet.lineDashLengths!) | |
} | |
else | |
{ | |
context.setLineDash(phase: 0.0, lengths: []) | |
} | |
context.setLineCap(dataSet.lineCapType) | |
// if drawing cubic lines is enabled | |
switch dataSet.mode | |
{ | |
case .linear: fallthrough | |
case .stepped: | |
drawLinear(context: context, dataSet: dataSet) | |
case .cubicBezier: | |
drawCubicBezier(context: context, dataSet: dataSet) | |
case .horizontalBezier: | |
drawHorizontalBezier(context: context, dataSet: dataSet) | |
} | |
context.restoreGState() | |
} | |
@objc open func drawCubicBezier(context: CGContext, dataSet: ILineChartDataSet) | |
{ | |
guard let dataProvider = dataProvider else { return } | |
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) | |
let phaseY = animator.phaseY | |
_xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) | |
// get the color that is specified for this position from the DataSet | |
let drawingColor = dataSet.colors.first! | |
let intensity = dataSet.cubicIntensity | |
// the path for the cubic-spline | |
let cubicPath = CGMutablePath() | |
let valueToPixelMatrix = trans.valueToPixelMatrix | |
if _xBounds.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 | |
// And in the `lastIndex`, add +1 | |
let firstIndex = _xBounds.min + 1 | |
let lastIndex = _xBounds.min + _xBounds.range | |
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 stride(from: firstIndex, through: lastIndex, by: 1) | |
{ | |
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) | |
} | |
} | |
context.saveGState() | |
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: _xBounds) | |
} | |
context.beginPath() | |
context.addPath(cubicPath) | |
context.setStrokeColor(drawingColor.cgColor) | |
context.strokePath() | |
context.restoreGState() | |
} | |
@objc open func drawHorizontalBezier(context: CGContext, dataSet: ILineChartDataSet) | |
{ | |
guard let dataProvider = dataProvider else { return } | |
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) | |
let phaseY = animator.phaseY | |
_xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) | |
// get the color that is specified for this position from the DataSet | |
let drawingColor = dataSet.colors.first! | |
// the path for the cubic-spline | |
let cubicPath = CGMutablePath() | |
let valueToPixelMatrix = trans.valueToPixelMatrix | |
if _xBounds.range >= 1 | |
{ | |
var prev: ChartDataEntry! = dataSet.entryForIndex(_xBounds.min) | |
var cur: ChartDataEntry! = prev | |
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 _xBounds.dropFirst() | |
{ | |
prev = cur | |
cur = dataSet.entryForIndex(j) | |
let cpx = CGFloat(prev.x + (cur.x - prev.x) / 2.0) | |
cubicPath.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) | |
} | |
} | |
context.saveGState() | |
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: _xBounds) | |
} | |
context.beginPath() | |
context.addPath(cubicPath) | |
context.setStrokeColor(drawingColor.cgColor) | |
context.strokePath() | |
context.restoreGState() | |
} | |
open func drawCubicFill( | |
context: CGContext, | |
dataSet: ILineChartDataSet, | |
spline: CGMutablePath, | |
matrix: CGAffineTransform, | |
bounds: XBounds) | |
{ | |
guard | |
let dataProvider = dataProvider | |
else { return } | |
if bounds.range <= 0 | |
{ | |
return | |
} | |
let fillMin = dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0 | |
var pt1 = CGPoint(x: CGFloat(dataSet.entryForIndex(bounds.min + bounds.range)?.x ?? 0.0), y: fillMin) | |
var pt2 = CGPoint(x: CGFloat(dataSet.entryForIndex(bounds.min)?.x ?? 0.0), y: fillMin) | |
pt1 = pt1.applying(matrix) | |
pt2 = pt2.applying(matrix) | |
spline.addLine(to: pt1) | |
spline.addLine(to: pt2) | |
spline.closeSubpath() | |
if dataSet.fill != nil | |
{ | |
drawFilledPath(context: context, path: spline, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha) | |
} | |
else | |
{ | |
drawFilledPath(context: context, path: spline, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha) | |
} | |
} | |
private var _lineSegments = [CGPoint](repeating: CGPoint(), count: 2) | |
@objc open 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 | |
_xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) | |
// if drawing filled is enabled | |
if dataSet.isDrawFilledEnabled && entryCount > 0 | |
{ | |
drawLinearFill(context: context, dataSet: dataSet, trans: trans, bounds: _xBounds) | |
} | |
context.saveGState() | |
// more than 1 color | |
if dataSet.colors.count > 1 | |
{ | |
if _lineSegments.count != pointsPerEntryPair | |
{ | |
// Allocate once in correct size | |
_lineSegments = [CGPoint](repeating: CGPoint(), count: pointsPerEntryPair) | |
} | |
for j in _xBounds | |
{ | |
var e: ChartDataEntry! = dataSet.entryForIndex(j) | |
if e == nil { continue } | |
_lineSegments[0].x = CGFloat(e.x) | |
_lineSegments[0].y = CGFloat(e.y * phaseY) | |
if j < _xBounds.max | |
{ | |
e = dataSet.entryForIndex(j + 1) | |
if e == nil { break } | |
if isDrawSteppedEnabled | |
{ | |
_lineSegments[1] = CGPoint(x: CGFloat(e.x), y: _lineSegments[0].y) | |
_lineSegments[2] = _lineSegments[1] | |
_lineSegments[3] = CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)) | |
} | |
else | |
{ | |
_lineSegments[1] = CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)) | |
} | |
} | |
else | |
{ | |
_lineSegments[1] = _lineSegments[0] | |
} | |
for i in 0..<_lineSegments.count | |
{ | |
_lineSegments[i] = _lineSegments[i].applying(valueToPixelMatrix) | |
} | |
if (!viewPortHandler.isInBoundsRight(_lineSegments[0].x)) | |
{ | |
break | |
} | |
// make sure the lines don't do shitty things outside bounds | |
if !viewPortHandler.isInBoundsLeft(_lineSegments[1].x) | |
|| (!viewPortHandler.isInBoundsTop(_lineSegments[0].y) && !viewPortHandler.isInBoundsBottom(_lineSegments[1].y)) | |
{ | |
continue | |
} | |
// get the color that is set for this line-segment | |
context.setStrokeColor(dataSet.color(atIndex: j).cgColor) | |
context.strokeLineSegments(between: _lineSegments) | |
} | |
} | |
else | |
{ // only one color per dataset | |
var e1: ChartDataEntry! | |
var e2: ChartDataEntry! | |
e1 = dataSet.entryForIndex(_xBounds.min) | |
if e1 != nil | |
{ | |
context.beginPath() | |
var firstPoint = true | |
for x in _xBounds | |
{ | |
e1 = dataSet.entryForIndex(x == 0 ? 0 : (x - 1)) | |
e2 = dataSet.entryForIndex(x) | |
if e1 == nil || e2 == nil { continue } | |
let pt = CGPoint( | |
x: CGFloat(e1.x), | |
y: CGFloat(e1.y * phaseY) | |
).applying(valueToPixelMatrix) | |
if firstPoint | |
{ | |
context.move(to: pt) | |
firstPoint = false | |
} | |
else | |
{ | |
let pt2 = CGPoint( | |
x: CGFloat(e2.x), | |
y: CGFloat(e2.y * phaseY) | |
).applying(valueToPixelMatrix) | |
if (e1.y == -999) { | |
context.move(to: pt2) | |
} | |
else | |
context.addLine(to: pt) | |
} | |
if isDrawSteppedEnabled | |
{ | |
context.addLine(to: CGPoint( | |
x: CGFloat(e2.x), | |
y: CGFloat(e1.y * phaseY) | |
).applying(valueToPixelMatrix)) | |
} | |
context.addLine(to: CGPoint( | |
x: CGFloat(e2.x), | |
y: CGFloat(e2.y * phaseY) | |
).applying(valueToPixelMatrix)) | |
} | |
if !firstPoint | |
{ | |
context.setStrokeColor(dataSet.color(atIndex: 0).cgColor) | |
context.strokePath() | |
} | |
} | |
} | |
context.restoreGState() | |
} | |
open func drawLinearFill(context: CGContext, dataSet: ILineChartDataSet, trans: Transformer, bounds: XBounds) | |
{ | |
guard let dataProvider = dataProvider else { return } | |
let filled = generateFilledPath( | |
dataSet: dataSet, | |
fillMin: dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0, | |
bounds: bounds, | |
matrix: trans.valueToPixelMatrix) | |
if dataSet.fill != nil | |
{ | |
drawFilledPath(context: context, path: filled, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha) | |
} | |
else | |
{ | |
drawFilledPath(context: context, path: filled, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha) | |
} | |
} | |
/// Generates the path that is used for filled drawing. | |
private func generateFilledPath(dataSet: ILineChartDataSet, fillMin: CGFloat, bounds: XBounds, matrix: CGAffineTransform) -> CGPath | |
{ | |
let phaseY = animator.phaseY | |
let isDrawSteppedEnabled = dataSet.mode == .stepped | |
let matrix = matrix | |
var e: ChartDataEntry! | |
let filled = CGMutablePath() | |
e = dataSet.entryForIndex(bounds.min) | |
if e != nil | |
{ | |
filled.move(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix) | |
filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix) | |
} | |
// create a new path | |
for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1) | |
{ | |
guard let e = dataSet.entryForIndex(x) else { continue } | |
if isDrawSteppedEnabled | |
{ | |
guard let ePrev = dataSet.entryForIndex(x-1) else { continue } | |
filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix) | |
} | |
filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix) | |
} | |
// close up | |
e = dataSet.entryForIndex(bounds.range + bounds.min) | |
if e != nil | |
{ | |
filled.addLine(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix) | |
} | |
filled.closeSubpath() | |
return filled | |
} | |
open override func drawValues(context: CGContext) | |
{ | |
guard | |
let dataProvider = dataProvider, | |
let lineData = dataProvider.lineData | |
else { return } | |
if isDrawingValuesAllowed(dataProvider: dataProvider) | |
{ | |
var dataSets = lineData.dataSets | |
let phaseY = animator.phaseY | |
var pt = CGPoint() | |
for i in 0 ..< dataSets.count | |
{ | |
guard let dataSet = dataSets[i] as? ILineChartDataSet else { continue } | |
if !shouldDrawValues(forDataSet: dataSet) | |
{ | |
continue | |
} | |
let valueFont = dataSet.valueFont | |
guard let formatter = dataSet.valueFormatter else { continue } | |
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) | |
let valueToPixelMatrix = trans.valueToPixelMatrix | |
let iconsOffset = dataSet.iconsOffset | |
// make sure the values do not interfear with the circles | |
var valOffset = Int(dataSet.circleRadius * 1.75) | |
if !dataSet.isDrawCirclesEnabled | |
{ | |
valOffset = valOffset / 2 | |
} | |
_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 | |
} | |
if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) | |
{ | |
continue | |
} | |
if dataSet.isDrawValuesEnabled { | |
ChartUtils.drawText( | |
context: context, | |
text: formatter.stringForValue( | |
e.y, | |
entry: e, | |
dataSetIndex: i, | |
viewPortHandler: viewPortHandler), | |
point: CGPoint( | |
x: pt.x, | |
y: pt.y - CGFloat(valOffset) - valueFont.lineHeight), | |
align: .center, | |
attributes: [NSAttributedString.Key.font: valueFont, NSAttributedString.Key.foregroundColor: dataSet.valueTextColorAt(j)]) | |
} | |
if let icon = e.icon, dataSet.isDrawIconsEnabled | |
{ | |
ChartUtils.drawImage(context: context, | |
image: icon, | |
x: pt.x + iconsOffset.x, | |
y: pt.y + iconsOffset.y, | |
size: icon.size) | |
} | |
} | |
} | |
} | |
} | |
open override func drawExtras(context: CGContext) | |
{ | |
drawCircles(context: context) | |
} | |
private func drawCircles(context: CGContext) | |
{ | |
guard | |
let dataProvider = dataProvider, | |
let lineData = dataProvider.lineData | |
else { return } | |
let phaseY = animator.phaseY | |
let dataSets = lineData.dataSets | |
var pt = CGPoint() | |
var rect = CGRect() | |
// If we redraw the data, remove and repopulate accessible elements to update label values and frames | |
accessibleChartElements.removeAll() | |
accessibilityOrderedElements = accessibilityCreateEmptyOrderedElements() | |
// Make the chart header the first element in the accessible elements array | |
if let chart = dataProvider as? LineChartView { | |
let element = createAccessibleHeader(usingChart: chart, | |
andData: lineData, | |
withDefaultDescription: "Line Chart") | |
accessibleChartElements.append(element) | |
} | |
context.saveGState() | |
for i in 0 ..< dataSets.count | |
{ | |
guard let dataSet = lineData.getDataSetByIndex(i) as? ILineChartDataSet else { continue } | |
if !dataSet.isVisible || dataSet.entryCount == 0 | |
{ | |
continue | |
} | |
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) | |
let valueToPixelMatrix = trans.valueToPixelMatrix | |
_xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) | |
let circleRadius = dataSet.circleRadius | |
let circleDiameter = circleRadius * 2.0 | |
let circleHoleRadius = dataSet.circleHoleRadius | |
let circleHoleDiameter = circleHoleRadius * 2.0 | |
let drawCircleHole = dataSet.isDrawCircleHoleEnabled && | |
circleHoleRadius < circleRadius && | |
circleHoleRadius > 0.0 | |
let drawTransparentCircleHole = drawCircleHole && | |
(dataSet.circleHoleColor == nil || | |
dataSet.circleHoleColor == NSUIColor.clear) | |
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 circles don't do shitty things outside bounds | |
if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) | |
{ | |
continue | |
} | |
// Accessibility element geometry | |
let scaleFactor: CGFloat = 3 | |
let accessibilityRect = CGRect(x: pt.x - (scaleFactor * circleRadius), | |
y: pt.y - (scaleFactor * circleRadius), | |
width: scaleFactor * circleDiameter, | |
height: scaleFactor * circleDiameter) | |
// Create and append the corresponding accessibility element to accessibilityOrderedElements | |
if let chart = dataProvider as? LineChartView | |
{ | |
let element = createAccessibleElement(withIndex: j, | |
container: chart, | |
dataSet: dataSet, | |
dataSetIndex: i) | |
{ (element) in | |
element.accessibilityFrame = accessibilityRect | |
} | |
accessibilityOrderedElements[i].append(element) | |
} | |
if !dataSet.isDrawCirclesEnabled | |
{ | |
continue | |
} | |
context.setFillColor(dataSet.getCircleColor(atIndex: j)!.cgColor) | |
rect.origin.x = pt.x - circleRadius | |
rect.origin.y = pt.y - circleRadius | |
rect.size.width = circleDiameter | |
rect.size.height = circleDiameter | |
if drawTransparentCircleHole | |
{ | |
// Begin path for circle with hole | |
context.beginPath() | |
context.addEllipse(in: rect) | |
// Cut hole in path | |
rect.origin.x = pt.x - circleHoleRadius | |
rect.origin.y = pt.y - circleHoleRadius | |
rect.size.width = circleHoleDiameter | |
rect.size.height = circleHoleDiameter | |
context.addEllipse(in: rect) | |
// Fill in-between | |
context.fillPath(using: .evenOdd) | |
} | |
else | |
{ | |
context.fillEllipse(in: rect) | |
if drawCircleHole | |
{ | |
context.setFillColor(dataSet.circleHoleColor!.cgColor) | |
// The hole rect | |
rect.origin.x = pt.x - circleHoleRadius | |
rect.origin.y = pt.y - circleHoleRadius | |
rect.size.width = circleHoleDiameter | |
rect.size.height = circleHoleDiameter | |
context.fillEllipse(in: rect) | |
} | |
} | |
} | |
} | |
context.restoreGState() | |
// Merge nested ordered arrays into the single accessibleChartElements. | |
accessibleChartElements.append(contentsOf: accessibilityOrderedElements.flatMap { $0 } ) | |
accessibilityPostLayoutChangedNotification() | |
} | |
open override func drawHighlighted(context: CGContext, indices: [Highlight]) | |
{ | |
guard | |
let dataProvider = dataProvider, | |
let lineData = dataProvider.lineData | |
else { return } | |
let chartXMax = dataProvider.chartXMax | |
context.saveGState() | |
for high in indices | |
{ | |
guard let set = lineData.getDataSetByIndex(high.dataSetIndex) as? ILineChartDataSet | |
, set.isHighlightEnabled | |
else { continue } | |
guard let e = set.entryForXValue(high.x, closestToY: high.y) else { continue } | |
if !isInBoundsX(entry: e, 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 = high.x // get the x-position | |
let y = high.y * Double(animator.phaseY) | |
if x > chartXMax * animator.phaseX | |
{ | |
continue | |
} | |
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() | |
} | |
/// Creates a nested array of empty subarrays each of which will be populated with NSUIAccessibilityElements. | |
/// This is marked internal to support HorizontalBarChartRenderer as well. | |
private func accessibilityCreateEmptyOrderedElements() -> [[NSUIAccessibilityElement]] | |
{ | |
guard let chart = dataProvider as? LineChartView else { return [] } | |
let dataSetCount = chart.lineData?.dataSetCount ?? 0 | |
return Array(repeating: [NSUIAccessibilityElement](), | |
count: dataSetCount) | |
} | |
/// Creates an NSUIAccessibleElement representing the smallest meaningful bar of the chart | |
/// i.e. in case of a stacked chart, this returns each stack, not the combined bar. | |
/// Note that it is marked internal to support subclass modification in the HorizontalBarChart. | |
private func createAccessibleElement(withIndex idx: Int, | |
container: LineChartView, | |
dataSet: ILineChartDataSet, | |
dataSetIndex: Int, | |
modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement | |
{ | |
let element = NSUIAccessibilityElement(accessibilityContainer: container) | |
let xAxis = container.xAxis | |
guard let e = dataSet.entryForIndex(idx) else { return element } | |
guard let dataProvider = dataProvider else { return element } | |
// NOTE: The formatter can cause issues when the x-axis labels are consecutive ints. | |
// i.e. due to the Double conversion, if there are more than one data set that are grouped, | |
// there is the possibility of some labels being rounded up. A floor() might fix this, but seems to be a brute force solution. | |
let label = xAxis.valueFormatter?.stringForValue(e.x, axis: xAxis) ?? "\(e.x)" | |
let elementValueText = dataSet.valueFormatter?.stringForValue(e.y, | |
entry: e, | |
dataSetIndex: dataSetIndex, | |
viewPortHandler: viewPortHandler) ?? "\(e.y)" | |
let dataSetCount = dataProvider.lineData?.dataSetCount ?? -1 | |
let doesContainMultipleDataSets = dataSetCount > 1 | |
element.accessibilityLabel = "\(doesContainMultipleDataSets ? (dataSet.label ?? "") + ", " : "") \(label): \(elementValueText)" | |
modifier(element) | |
return element | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment