【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线

【DGCharts】iOS 图表渲染事实标准------8 种图表类型、高度可定制,3 行代码画出一条折线

iOS三方库精读 · 第 11 期


一、一句话介绍

DGCharts(原名 Charts)是一个用于 iOS/macOS/tvOS 的图表渲染库,它是 Android 端 MPAndroidChart 的 Swift 移植版,让在 UIKit 与 SwiftUI 中绘制专业级折线图、柱状图、饼图等 8 种图表类型变得像配置数据一样简单。

属性 信息
⭐ GitHub Stars 27.5k+
最新稳定版 5.1.0
License Apache 2.0
支持平台 iOS 13+ / macOS 10.14+ / tvOS 13+
语言 Swift(纯 Swift,无 OC 接口)

二、为什么选择它

原生痛点

苹果官方没有提供专用图表框架(Swift Charts 直到 iOS 16 才随 SwiftUI 一起发布),在此之前,iOS 开发者要想画一条像样的折线图,要么:

  • 用 CoreGraphics 手撸贝塞尔曲线 → 代码量大,动画难实现
  • 用 CALayer + CAAnimation → 正确实现坐标系变换极其麻烦
  • 引入 WebView 渲染 ECharts → 性能差、交互延迟

DGCharts 解决的核心痛点:

  1. 零 CoreGraphics 基础可上手:数据 → DataSet → Chart,三行完成
  2. 内置 8 种图表类型:折线、柱状、饼、雷达、散点、K 线、气泡、组合图
  3. 原生手势支持:缩放、拖拽、高亮点击,全部开箱即用
  4. 可定制到像素级别:颜色、字体、轴线、动画均可覆盖
  5. iOS 13+,Swift Charts 的前辈:大量存量 App 仍在使用,理解它有助于迁移评估

与 Swift Charts 的核心区别(一眼对比)

维度 DGCharts Swift Charts(苹果官方)
平台要求 iOS 13+ iOS 16+
UI 框架 UIKit + SwiftUI SwiftUI only
图表类型 8 种(含 K 线/雷达) 折/柱/面积/点(4 种)
动画控制 精细帧级控制 声明式,较难定制
手势交互 内置缩放/平移 需自己实现
维护状态 社区维护,活跃 Apple 官方,稳定更新

三、核心功能速览

基础层(新手必读)

环境集成

SPM(推荐):

swift 复制代码
// Package.swift 或 Xcode Add Package Dependency
// URL: https://github.com/danielgindi/Charts.git
// from: "5.1.0"

CocoaPods:

ruby 复制代码
pod 'DGCharts', '~> 5.1.0'
最简用法:3 行画折线图
swift 复制代码
import DGCharts

let chartView = LineChartView(frame: view.bounds)

// 准备数据
let entries = (0..<10).map { ChartDataEntry(x: Double($0), y: Double.random(in: 10...100)) }
let dataSet = LineChartDataSet(entries: entries, label: "销售额")
chartView.data = LineChartData(dataSet: dataSet)

view.addSubview(chartView)

基础层 概念:ChartDataEntry 是最小数据单元(x, y),XxxChartDataSet 是同类数据的集合(样式配置在这里),XxxChartData 是图表的数据容器,XxxChartView 是最终渲染视图。


进阶层(最佳实践)

8 种图表类型一览
类名 用途
LineChartView 折线图(支持曲线插值)
BarChartView 柱状图(支持堆叠/分组)
PieChartView 饼图 / 甜甜圈图
RadarChartView 雷达图(蜘蛛网图)
ScatterChartView 散点图
CandleStickChartView K 线图(OHLC)
BubbleChartView 气泡图
CombinedChartView 组合图(折+柱叠加)
常用样式定制
swift 复制代码
// 折线数据集样式
let dataSet = LineChartDataSet(entries: entries, label: "趋势")
dataSet.colors = [.systemBlue]          // 线条颜色
dataSet.lineWidth = 2.0                  // 线宽
dataSet.circleColors = [.systemBlue]     // 数据点颜色
dataSet.circleRadius = 4.0               // 数据点半径
dataSet.drawFilledEnabled = true         // 开启渐变填充
dataSet.fillColor = .systemBlue
dataSet.fillAlpha = 0.3
dataSet.mode = .cubicBezier              // 贝塞尔平滑曲线

// 图表视图样式
lineChartView.rightAxis.enabled = false  // 隐藏右轴
lineChartView.xAxis.labelPosition = .bottom
lineChartView.animate(xAxisDuration: 1.0, easingOption: .easeInOutQuart)
交互设置
swift 复制代码
chartView.scaleXEnabled = true           // 允许 X 轴缩放
chartView.scaleYEnabled = false          // 禁止 Y 轴缩放
chartView.dragEnabled = true             // 允许拖拽
chartView.pinchZoomEnabled = true        // 双指缩放
chartView.doubleTapToZoomEnabled = false // 关闭双击缩放
X 轴自定义 Formatter
swift 复制代码
// 将数字索引转为日期字符串
class DateAxisValueFormatter: AxisValueFormatter {
    private let dates = ["1月", "2月", "3月", "4月", "5月", "6月"]
    func stringForValue(_ value: Double, axis: AxisBase?) -> String {
        let index = Int(value)
        return index < dates.count ? dates[index] : ""
    }
}
chartView.xAxis.valueFormatter = DateAxisValueFormatter()

深入层(源码视角)

DGCharts 的核心渲染架构遵循 Renderer 模式

  • ChartView → 入口,持有 ChartData 和多个 DataRenderer
  • DataRenderer(如 LineChartRenderer)→ 负责具体图形绘制,使用 CGContext
  • ChartViewBase → 提供手势识别、坐标变换 Transformer
  • Transformer → 负责将数据坐标系映射到屏幕坐标系(核心矩阵变换)

每次 data 被 set,触发 notifyDataSetChanged()calcMinMax()setNeedsDisplay(),最终回调 draw(_ rect:) 由各 Renderer 完成绘制。


四、实战演示

场景:股票日 K 线 + 成交量组合图

swift 复制代码
// Swift UIKit,iOS 17+
import UIKit
import DGCharts

class StockChartVC: UIViewController {

    private lazy var combinedChart: CombinedChartView = {
        let chart = CombinedChartView()
        chart.legend.enabled = true
        chart.rightAxis.enabled = false
        chart.xAxis.labelPosition = .bottom
        chart.animate(xAxisDuration: 0.8)
        return chart
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        view.addSubview(combinedChart)
        combinedChart.frame = CGRect(x: 16, y: 100,
                                      width: view.bounds.width - 32,
                                      height: 320)
        setupChart()
    }

    private func setupChart() {
        // ---- K 线数据 ----
        let candleEntries: [CandleChartDataEntry] = [
            CandleChartDataEntry(x: 0, shadowH: 185, shadowL: 162, open: 165, close: 178),
            CandleChartDataEntry(x: 1, shadowH: 190, shadowL: 170, open: 178, close: 188),
            CandleChartDataEntry(x: 2, shadowH: 195, shadowL: 175, open: 188, close: 177),
            CandleChartDataEntry(x: 3, shadowH: 182, shadowL: 165, open: 177, close: 169),
            CandleChartDataEntry(x: 4, shadowH: 175, shadowL: 160, open: 169, close: 172),
        ]
        let candleSet = CandleChartDataSet(entries: candleEntries, label: "K 线")
        candleSet.shadowColor = .darkGray
        candleSet.shadowWidth = 1.5
        candleSet.decreasingColor = .systemRed     // 阴线颜色
        candleSet.decreasingFilled = true
        candleSet.increasingColor = .systemGreen   // 阳线颜色
        candleSet.increasingFilled = true
        candleSet.neutralColor = .systemGray

        // ---- 成交量柱状图 ----
        let barEntries = [
            BarChartDataEntry(x: 0, y: 3200),
            BarChartDataEntry(x: 1, y: 5400),
            BarChartDataEntry(x: 2, y: 4100),
            BarChartDataEntry(x: 3, y: 6800),
            BarChartDataEntry(x: 4, y: 3900),
        ]
        let barSet = BarChartDataSet(entries: barEntries, label: "成交量(手)")
        barSet.colors = [.systemBlue.withAlphaComponent(0.5)]
        barSet.axisDependency = .right  // 绑定右轴(虽然禁用了,仅用于数据缩放)

        // ---- 组合 ----
        let combined = CombinedChartData()
        combined.candleData = CandleChartData(dataSet: candleSet)
        combined.barData = BarChartData(dataSet: barSet)

        // X 轴 Formatter
        combinedChart.xAxis.valueFormatter = IndexAxisValueFormatter(
            values: ["Mon", "Tue", "Wed", "Thu", "Fri"]
        )
        combinedChart.xAxis.granularity = 1
        combinedChart.data = combined
    }
}

五、源码亮点

进阶层:值得借鉴的用法

MarkerView------点击数据点显示气泡
swift 复制代码
// 自定义 Marker,继承 MarkerView
class BalloonMarker: MarkerView {
    private var label: String = ""

    override func refreshContent(entry: ChartDataEntry, highlight: Highlight) {
        label = String(format: "%.1f", entry.y)
        super.refreshContent(entry: entry, highlight: highlight)
    }

    override func draw(context: CGContext, point: CGPoint) {
        // 用 CoreGraphics 绘制气泡背景 + 文字
        let attrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 12),
            .foregroundColor: UIColor.white
        ]
        let attrStr = NSAttributedString(string: label, attributes: attrs)
        let size = attrStr.size()
        let rect = CGRect(x: point.x - size.width / 2 - 8,
                          y: point.y - size.height - 12,
                          width: size.width + 16, height: size.height + 8)
        context.setFillColor(UIColor.systemBlue.cgColor)
        UIBezierPath(roundedRect: rect, cornerRadius: 6).fill()
        attrStr.draw(in: rect.insetBy(dx: 8, dy: 4))
    }
}

// 绑定
chartView.marker = BalloonMarker()
chartView.drawMarkers = true

深入层:设计思想解析

1. Transformer 矩阵变换

DGCharts 用一个 CGAffineTransform 维护数据坐标 → 屏幕坐标的映射:

css 复制代码
屏幕坐标 = 数据坐标 × scale + offset + chart padding

每次缩放/平移,只更新 transform 矩阵然后 setNeedsDisplay,避免重新计算所有点,是渲染性能的核心保障。

2. Protocol-Oriented Renderer
swift 复制代码
// 每种图表有独立 Renderer,统一协议
public protocol DataRenderer: AnyObject {
    func drawData(context: CGContext)
    func drawValues(context: CGContext)
    func drawExtras(context: CGContext)
    func drawHighlighted(context: CGContext, indices: [Highlight])
}

遵循开闭原则,新增图表类型只需实现此协议,不修改任何现有代码。

3. ChartHighlighter --- 触摸高亮的寻值逻辑

触摸事件 → getHighlight(x:y:) → 二分查找最近 x → 在该 x 的所有 DataSet 中找最近 y → 返回 Highlight(x:y:dataSetIndex:) → 触发 highlightValue。整套流程在主线程同步完成,保证交互响应 < 16ms。


六、踩坑记录

问题 1:数据更新后图表没有刷新

  • 原因 :修改了 entries 数组但没有通知图表

  • 解决

    swift 复制代码
    chartView.data?.notifyDataChanged()
    chartView.notifyDataSetChanged()

问题 2:X 轴标签显示 0, 1, 2 而不是自定义文字

  • 原因 :没有设置 xAxis.valueFormatter 或者 granularity 不正确

  • 解决

    swift 复制代码
    xAxis.valueFormatter = IndexAxisValueFormatter(values: yourLabels)
    xAxis.granularity = 1.0   // 必须设置,否则可能跳步

问题 3:饼图中间的 "洞" 大小无法控制

  • 原因holeRadiusPercent 默认 0.5(即 50%)

  • 解决

    swift 复制代码
    pieChart.holeRadiusPercent = 0.3  // 调整洞的比例,0 = 实心饼图

问题 4:动画结束后视图突然闪烁

  • 原因animateXanimateY 同时调用,两个动画结束时各触发一次 setNeedsDisplay
  • 解决 :使用 animate(xAxisDuration:yAxisDuration:) 合并为一次调用

问题 5:在 SwiftUI 中使用时手势冲突

  • 原因:DGCharts 的 UIPanGestureRecognizer 与 SwiftUI ScrollView 的手势抢夺

  • 解决

    swift 复制代码
    chartView.dragEnabled = false       // 在 ScrollView 内关闭拖拽
    chartView.pinchZoomEnabled = false  // 关闭缩放

问题 6:Swift Package Manager 拉取超时

  • 原因:Charts 仓库体积较大(含所有 Demo)
  • 解决 :在 package.json 中指定 .upToNextMinor(from: "5.1.0"),避免每次重新解析

七、延伸思考

同类库对比

维度 DGCharts Swift Charts (Apple) AAChartKit Charts.js (WebView)
平台要求 iOS 13+ iOS 16+ iOS 11+ iOS (via WKWebView)
图表类型数 8 4 10+ 20+
性能 原生,高 原生,高 原生,良好 WebView,差
K 线图
SwiftUI 支持 UIViewRepresentable 原生 UIViewRepresentable WKWebViewRepresentable
维护状态 社区活跃 Apple 官方 活跃 Web 项目
学习曲线 中等 低(SwiftUI 声明式) 需了解 JS

推荐使用场景

  • ✅ iOS 13~15 的存量 App,无法使用 Swift Charts
  • ✅ 需要 K 线图、雷达图等特殊类型
  • ✅ 需要精细控制动画和交互手势
  • ✅ 同时支持 iOS 和 macOS 的多平台 App

不推荐场景

  • ❌ 全新 App,最低支持 iOS 16+,且只用 SwiftUI → 直接用 Swift Charts
  • ❌ 只需要一个简单静态饼图/柱图 → 手动 CoreGraphics 更轻量
  • ❌ 需要超复杂的动态可视化(如力导向图)→ 考虑 WebView + D3.js

八、参考资源


九、本期互动

小作业

使用 LineChartView 实现一个实时动态折线图:每秒追加一个随机数据点,并保持最多显示最近 20 个点(超出则移除最旧的点),同时保证图表 X 轴自动滚动跟随最新数据。完成后在评论区贴出你的核心更新逻辑。

思考题

DGCharts 的 Transformer 用矩阵变换把数据坐标映射到屏幕坐标,这种设计和 CALayertransform 有何本质区别?如果让你自己实现一套坐标系映射,你会选择哪种方案?为什么?

读者征集

下一期我们将深入 Hero(转场动画库)。你在项目中用过哪些自定义转场方案(Hero / UIViewControllerTransitioningDelegate / NavigationController Push 动画)?欢迎评论区分享你的实践经验,优质回答会收录进下一期《踩坑记录》。


📅 本系列每周五晚更新 ✅ 第1期:Alamofire · ✅ 第2期:SDWebImage · ✅ 第3期:Kingfisher · ✅ 第4期:SnapKit · ✅ 第5期:ListDiff · ✅ 第6期:RxSwift · ✅ 第7期:Lottie · ✅ 第8期:MarkdownUI · ✅ 第9期:AFNetworking · ➡️ 第11期:DGCharts · ○ 第12期:Hero

相关推荐
chaoguo123415 小时前
Any metadata 的内存布局
swift·metadata·value witness table
tangweiguo030519872 天前
SwiftUI布局完全指南:从入门到精通
ios·swift
用户79457223954132 天前
【RxSwift】Swift 版 ReactiveX,响应式编程优雅处理异步事件流
swift·rxswift
战族狼魂3 天前
XCode 发起视频 和 收到视频通话邀请实现双语功能 中文和俄语
swift
UXbot3 天前
2026年AI全链路产品开发工具对比:5款从创意到上线一站式平台深度解析
前端·ui·kotlin·软件构建·swift·原型模式
报错小能手3 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
报错小能手4 天前
ios开发方向——swift并发进阶核心 async/await 详解
开发语言·ios·swift
用户79457223954134 天前
【Lottie】让设计稿上的动效直接"活"在 App 里
swiftui·swift
Mr_Tony6 天前
Swift 中的 Combine 框架完整指南(含示例代码 + 实战)
开发语言·swift