You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在iOS-Charts中添加最大值与最小值标记箭头?

在iOS-Charts中添加最大值与最小值标记箭头的实现思路

嘿,刚好我之前做过类似的需求,给你分享几个靠谱的实现思路,都能达到你要的箭头标记效果:

思路一:自定义Marker实现

Charts自带的Marker机制可以用来显示自定义标记,我们可以扩展Marker类来绘制箭头和数值标签:

  1. 定位最大/最小值Entry
    先从数据集中找到对应的最大和最小值条目:

    guard let lineChartDataSet = lineChartView.data?.dataSets.first as? LineChartDataSet,
          let maxEntry = lineChartDataSet.maxYEntry,
          let minEntry = lineChartDataSet.minYEntry else { return }
    
  2. 自定义带箭头的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()
        }
    }
    
  3. 绑定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:

  1. 创建自定义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)
        }
    }
    
  2. 使用自定义ChartView
    在Storyboard或者代码中替换原来的LineChartView为CustomArrowLineChartView,加载数据后就会自动绘制箭头。

思路三:利用ChartAnnotation组件实现

Charts的Annotation是官方提供的标注组件,适合管理多个自定义标记:

  1. 自定义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)
        }
    }
    
  2. 添加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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 12:06:17