【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 解决的核心痛点:
- 零 CoreGraphics 基础可上手:数据 → DataSet → Chart,三行完成
- 内置 8 种图表类型:折线、柱状、饼、雷达、散点、K 线、气泡、组合图
- 原生手势支持:缩放、拖拽、高亮点击,全部开箱即用
- 可定制到像素级别:颜色、字体、轴线、动画均可覆盖
- iOS 13+,Swift Charts 的前辈:大量存量 App 仍在使用,理解它有助于迁移评估
与 Swift Charts 的核心区别(一眼对比)
| 维度 | DGCharts | Swift Charts(苹果官方) |
|---|---|---|
| 平台要求 | iOS 13+ | iOS 16+ |
| UI 框架 | UIKit + SwiftUI | SwiftUI only |
| 图表类型 | 8 种(含 K 线/雷达) | 折/柱/面积/点(4 种) |
| 动画控制 | 精细帧级控制 | 声明式,较难定制 |
| 手势交互 | 内置缩放/平移 | 需自己实现 |
| 维护状态 | 社区维护,活跃 | Apple 官方,稳定更新 |
三、核心功能速览
基础层(新手必读)
环境集成
SPM(推荐):
// Package.swift 或 Xcode Add Package Dependency
// URL: https://github.com/danielgindi/Charts.git
// from: "5.1.0"
CocoaPods:
pod 'DGCharts', '~> 5.1.0'
最简用法:3 行画折线图
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 |
组合图(折+柱叠加) |
常用样式定制
// 折线数据集样式
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)
交互设置
chartView.scaleXEnabled = true // 允许 X 轴缩放
chartView.scaleYEnabled = false // 禁止 Y 轴缩放
chartView.dragEnabled = true // 允许拖拽
chartView.pinchZoomEnabled = true // 双指缩放
chartView.doubleTapToZoomEnabled = false // 关闭双击缩放
X 轴自定义 Formatter
// 将数字索引转为日期字符串
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 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——点击数据点显示气泡
// 自定义 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 维护数据坐标 → 屏幕坐标的映射:
屏幕坐标 = 数据坐标 × scale + offset + chart padding
每次缩放/平移,只更新 transform 矩阵然后 setNeedsDisplay,避免重新计算所有点,是渲染性能的核心保障。
2. Protocol-Oriented Renderer
// 每种图表有独立 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数组但没有通知图表 -
解决:
chartView.data?.notifyDataChanged() chartView.notifyDataSetChanged()
问题 2:X 轴标签显示 0, 1, 2 而不是自定义文字
-
原因:没有设置
xAxis.valueFormatter或者granularity不正确 -
解决:
xAxis.valueFormatter = IndexAxisValueFormatter(values: yourLabels) xAxis.granularity = 1.0 // 必须设置,否则可能跳步
问题 3:饼图中间的 "洞" 大小无法控制
-
原因:
holeRadiusPercent默认 0.5(即 50%) -
解决:
pieChart.holeRadiusPercent = 0.3 // 调整洞的比例,0 = 实心饼图
问题 4:动画结束后视图突然闪烁
-
原因:
animateX和animateY同时调用,两个动画结束时各触发一次setNeedsDisplay -
解决:使用
animate(xAxisDuration:yAxisDuration:)合并为一次调用
问题 5:在 SwiftUI 中使用时手势冲突
- 原因:DGCharts 的 UIPanGestureRecognizer 与 SwiftUI ScrollView 的手势抢夺
-
解决:
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
八、参考资源
- GitHub: danielgindi/Charts
- 官方 Demo App
- Apple Swift Charts 官方文档
- WWDC 2022 Session 10137 - Hello Swift Charts
- MPAndroidChart(Android 原版)
- 系列 Demo 仓库:
github.com/yourname/ios-lib-demos
九、本期互动
小作业
使用 LineChartView 实现一个实时动态折线图:每秒追加一个随机数据点,并保持最多显示最近 20 个点(超出则移除最旧的点),同时保证图表 X 轴自动滚动跟随最新数据。完成后在评论区贴出你的核心更新逻辑。
思考题
DGCharts 的 Transformer 用矩阵变换把数据坐标映射到屏幕坐标,这种设计和 CALayer 的 transform 有何本质区别?如果让你自己实现一套坐标系映射,你会选择哪种方案?为什么?
读者征集
下一期我们将深入 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