如何在iOS-Charts中添加最大值与最小值标记箭头?
在iOS-Charts中添加最大值与最小值标记箭头的实现思路
嘿,刚好我之前做过类似的需求,给你分享几个靠谱的实现思路,都能达到你要的箭头标记效果:
思路一:自定义Marker实现
Charts自带的Marker机制可以用来显示自定义标记,我们可以扩展Marker类来绘制箭头和数值标签:
定位最大/最小值Entry
先从数据集中找到对应的最大和最小值条目:guard let lineChartDataSet = lineChartView.data?.dataSets.first as? LineChartDataSet, let maxEntry = lineChartDataSet.maxYEntry, let minEntry = lineChartDataSet.minYEntry else { return }自定义带箭头的Marker类
继承MarkerView,用Core Graphics绘制箭头和标签:class ArrowMarker: MarkerView { var isMaxMarker: Bool = false override func draw(context: CGContext, point: CGPoint) { super.draw(context: context, point: point) context.saveGState() // 绘制箭头线 let arrowEndPoint = isMaxMarker ? CGPoint(x: point.x, y: point.y - 20) : CGPoint(x: point.x, y: point.y + 20) let linePath = UIBezierPath() linePath.move(to: point) linePath.addLine(to: arrowEndPoint) UIColor.systemRed.setStroke() linePath.lineWidth = 2 linePath.stroke() // 绘制箭头三角头部 let arrowWidth: CGFloat = 8 let trianglePath = UIBezierPath() trianglePath.move(to: arrowEndPoint) if isMaxMarker { trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x - arrowWidth, y: arrowEndPoint.y + arrowWidth)) trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x + arrowWidth, y: arrowEndPoint.y + arrowWidth)) } else { trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x - arrowWidth, y: arrowEndPoint.y - arrowWidth)) trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x + arrowWidth, y: arrowEndPoint.y - arrowWidth)) } trianglePath.close() UIColor.systemRed.setFill() trianglePath.fill() // 绘制数值标签 let valueText = String(format: "%.1f", entry?.y ?? 0) let textAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12), NSAttributedString.Key.foregroundColor: UIColor.systemRed] let textSize = valueText.size(withAttributes: textAttributes) let textOffset: CGFloat = 5 let textPoint = isMaxMarker ? CGPoint(x: arrowEndPoint.x - textSize.width/2, y: arrowEndPoint.y - textSize.height - textOffset) : CGPoint(x: arrowEndPoint.x - textSize.width/2, y: arrowEndPoint.y + textOffset) valueText.draw(at: textPoint, withAttributes: textAttributes) context.restoreGState() } }绑定Marker到图表
初始化Marker并关联到对应Entry,添加到图表中:// 最大值Marker let maxMarker = ArrowMarker() maxMarker.isMaxMarker = true maxMarker.chartView = lineChartView maxMarker.refreshContent(entry: maxEntry, highlight: Highlight(x: maxEntry.x, y: maxEntry.y, dataSetIndex: 0)) lineChartView.marker = maxMarker // 最小值Marker(若需同时显示两个,建议自定义组合Marker或使用思路二) let minMarker = ArrowMarker() minMarker.isMaxMarker = false minMarker.chartView = lineChartView minMarker.refreshContent(entry: minEntry, highlight: Highlight(x: minEntry.x, y: minEntry.y, dataSetIndex: 0))
思路二:重写ChartView的draw方法手动绘制
这种方式更灵活,直接在ChartView子类中绘制箭头,不需要依赖Marker:
创建自定义LineChartView子类
class CustomArrowLineChartView: LineChartView { override func draw(_ rect: CGRect) { super.draw(rect: rect) guard let dataSet = self.data?.dataSets.first as? LineChartDataSet, let maxEntry = dataSet.maxYEntry, let minEntry = dataSet.minYEntry else { return } // 将Entry坐标转换为图表像素点 let maxPixelPoint = self.getPixelForValues(x: maxEntry.x, y: maxEntry.y) let minPixelPoint = self.getPixelForValues(x: minEntry.x, y: minEntry.y) let context = UIGraphicsGetCurrentContext()! context.saveGState() // 绘制最大值箭头和标签 drawArrowWithValue(at: maxPixelPoint, value: maxEntry.y, isMax: true, context: context) // 绘制最小值箭头和标签 drawArrowWithValue(at: minPixelPoint, value: minEntry.y, isMax: false, context: context) context.restoreGState() } private func drawArrowWithValue(at point: CGPoint, value: Double, isMax: Bool, context: CGContext) { let arrowLength: CGFloat = 20 let arrowEndPoint = isMax ? CGPoint(x: point.x, y: point.y - arrowLength) : CGPoint(x: point.x, y: point.y + arrowLength) // 绘制箭头线 let linePath = UIBezierPath() linePath.move(to: point) linePath.addLine(to: arrowEndPoint) UIColor.systemRed.setStroke() linePath.lineWidth = 2 linePath.stroke() // 绘制箭头三角 let arrowWidth: CGFloat = 8 let trianglePath = UIBezierPath() trianglePath.move(to: arrowEndPoint) if isMax { trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x - arrowWidth, y: arrowEndPoint.y + arrowWidth)) trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x + arrowWidth, y: arrowEndPoint.y + arrowWidth)) } else { trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x - arrowWidth, y: arrowEndPoint.y - arrowWidth)) trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x + arrowWidth, y: arrowEndPoint.y - arrowWidth)) } trianglePath.close() UIColor.systemRed.setFill() trianglePath.fill() // 绘制数值标签 let valueText = String(format: "%.1f", value) let textAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12), NSAttributedString.Key.foregroundColor: UIColor.systemRed] let textSize = valueText.size(withAttributes: textAttributes) let textOffset: CGFloat = 5 let textPoint = isMax ? CGPoint(x: arrowEndPoint.x - textSize.width/2, y: arrowEndPoint.y - textSize.height - textOffset) : CGPoint(x: arrowEndPoint.x - textSize.width/2, y: arrowEndPoint.y + textOffset) valueText.draw(at: textPoint, withAttributes: textAttributes) } }使用自定义ChartView
在Storyboard或者代码中替换原来的LineChartView为CustomArrowLineChartView,加载数据后就会自动绘制箭头。
思路三:利用ChartAnnotation组件实现
Charts的Annotation是官方提供的标注组件,适合管理多个自定义标记:
自定义ArrowAnnotation类
class ArrowAnnotation: ChartAnnotation { var targetEntry: ChartDataEntry var isMax: Bool init(entry: ChartDataEntry, isMax: Bool) { self.targetEntry = entry self.isMax = isMax super.init() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func draw(context: CGContext, point: CGPoint) { let arrowLength: CGFloat = 20 let arrowEndPoint = isMax ? CGPoint(x: point.x, y: point.y - arrowLength) : CGPoint(x: point.x, y: point.y + arrowLength) let linePath = UIBezierPath() linePath.move(to: point) linePath.addLine(to: arrowEndPoint) UIColor.systemRed.setStroke() linePath.lineWidth = 2 linePath.stroke() let arrowWidth: CGFloat = 8 let trianglePath = UIBezierPath() trianglePath.move(to: arrowEndPoint) if isMax { trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x - arrowWidth, y: arrowEndPoint.y + arrowWidth)) trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x + arrowWidth, y: arrowEndPoint.y + arrowWidth)) } else { trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x - arrowWidth, y: arrowEndPoint.y - arrowWidth)) trianglePath.addLine(to: CGPoint(x: arrowEndPoint.x + arrowWidth, y: arrowEndPoint.y - arrowWidth)) } trianglePath.close() UIColor.systemRed.setFill() trianglePath.fill() let valueText = String(format: "%.1f", targetEntry.y) let textAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12), NSAttributedString.Key.foregroundColor: UIColor.systemRed] let textSize = valueText.size(withAttributes: textAttributes) let textOffset: CGFloat = 5 let textPoint = isMax ? CGPoint(x: arrowEndPoint.x - textSize.width/2, y: arrowEndPoint.y - textSize.height - textOffset) : CGPoint(x: arrowEndPoint.x - textSize.width/2, y: arrowEndPoint.y + textOffset) valueText.draw(at: textPoint, withAttributes: textAttributes) } }添加Annotation到图表
guard let dataSet = lineChartView.data?.dataSets.first as? LineChartDataSet, let maxEntry = dataSet.maxYEntry, let minEntry = dataSet.minYEntry else { return } // 添加最大值Annotation let maxAnnotation = ArrowAnnotation(entry: maxEntry, isMax: true) maxAnnotation.setPosition(x: maxEntry.x, y: maxEntry.y) lineChartView.addAnnotation(maxAnnotation) // 添加最小值Annotation let minAnnotation = ArrowAnnotation(entry: minEntry, isMax: false) minAnnotation.setPosition(x: minEntry.x, y: minEntry.y) lineChartView.addAnnotation(minAnnotation)
注意事项
- 可以根据需求调整箭头的颜色、长度、宽度,以及标签的字体样式
- 如果是动态更新数据,记得在数据刷新后重新触发绘制(比如思路二中调用
setNeedsDisplay(),思路三中重新添加Annotation) - 要处理数据集为空的边界情况,避免崩溃
内容的提问来源于stack exchange,提问作者Robin
相关产品推荐
相关产品推荐

