D15 - 用 Swift 和公开资讯,打造投资理财的 Apps { 加权指数K线图实作.3 - 使用 Charts 实作 K 线图 }

如果你真的对画图很有兴趣,而且很想自己做图表的类别,那你可以使用程序在拿到资料後,用 UIView, CALayer 这些物件,画出你要的图案。但这边介绍 Android, iOS 双平台都知名的 Charts 套件,作者不同人,但 iOS Charts 的套件在 readme 上有写,iOS 的套件是参考 Android 的 API 写的,所以在说明文件的连结上,也直接指向 Android Charts 套件的说明文件。

iOS 套件的 GitHub 如下

https://github.com/danielgindi/Charts

Android 套件的 GitHub 如下

https://github.com/PhilJay/MPAndroidChart

说明文件

https://weeklycoding.com/mpandroidchart/

安装套件的方法

先更新 Podfile

# Uncomment the next line to define a global platform for your project
platform :ios, '14.0'

target 'ITIronMan' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for ITIronMan
  pod 'Alamofire'
  pod 'SwiftCSV'
  pod 'Charts'

  target 'ITIronManTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

然後用 terminal 进入专案资料夹,输入 pod install 等到跑完,Charts 就装好了。

独立开一个 KLineViewController,先放在 KLine.storyboard 里,并开出放 K Line Model 的 array,让 parent 传进来。目前 parent 暂定为下载台股加权开高低收的 VC,因为那个 VC 会有资料。

下方的蓝色区域,就是 Chart 图要放的位置

https://ithelp.ithome.com.tw/upload/images/20210924/20140622PmwcqrJFGn.png

import UIKit
import Charts

class KLineViewController: UIViewController {
    
    @IBOutlet weak var chartContainer: UIView!
    
    var kLineDataSet = [StockKLine]()

    // MARK: - life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

在 TwStockMarketKLineViewController 加一个按钮,并让按钮发动转场,推入 KLineViewController

@IBAction func pushKLineButtonDidTap(_ sender: Any) {
        
        let storyboard = UIStoryboard(name: "KLine", bundle: nil)
        if let vc = storyboard.instantiateViewController(withIdentifier: "KLineViewController") as? KLineViewController {
            
            vc.kLineDataSet = model.twExStockDataSet //将 K 线资料传进 KLineViewController
            navigationController?.pushViewController(vc, animated: true)
        }
    }

在 viewDidLoad() 时,将 chartView 放进 container 里面,并把 autolayout 设定好。

整个 VC 的程序码如下

import UIKit
import Charts

class KLineViewController: UIViewController {
    
    @IBOutlet weak var chartContainer: UIView!
    
    private lazy var chartView: CandleStickChartView = {
        let view = CandleStickChartView()
        return view
    }()
    
    var kLineDataSet = [StockKLine]()

    // MARK: - life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBasicUI()
        setupCandleStickView()
    }
    
    // MARK: - private methods
    private func setupBasicUI() {
        
        chartContainer.backgroundColor = .clear
        
        chartContainer.addSubview(chartView)
        chartView.translatesAutoresizingMaskIntoConstraints = false
        
        chartView.leadingAnchor.constraint(equalTo: chartContainer.leadingAnchor).isActive = true
        chartView.topAnchor.constraint(equalTo: chartContainer.topAnchor).isActive = true
        chartView.trailingAnchor.constraint(equalTo: chartContainer.trailingAnchor).isActive = true
        chartView.bottomAnchor.constraint(equalTo: chartContainer.bottomAnchor).isActive = true
    }
    
    // 调整 Candle Stick View 的设定
    private func setupCandleStickView() {
        
        chartView.dragEnabled = false
        chartView.setScaleEnabled(true)
        chartView.maxVisibleCount = 200
        chartView.pinchZoomEnabled = true
        
        chartView.legend.horizontalAlignment = .right
        chartView.legend.verticalAlignment = .top
        chartView.legend.orientation = .vertical
        chartView.legend.drawInside = false
        chartView.legend.font = UIFont.systemFont(ofSize: 10)
        
        chartView.leftAxis.labelFont = UIFont.systemFont(ofSize: 10)
        chartView.leftAxis.spaceTop = 0.3
        chartView.leftAxis.spaceBottom = 0.3
        chartView.leftAxis.axisMinimum = 0
        
        chartView.rightAxis.enabled = false
        
        chartView.xAxis.labelPosition = .bottom
        chartView.xAxis.labelFont = UIFont.systemFont(ofSize: 10)
        
        chartView.maxVisibleCount = 20
    }
}

接下来实作将 K 线的资料转换成 CandleChart 的部分。

在 Charts 套件里面,将 Data Model 转成 ChartView 可以用的 data 类别,会需要经过下列的转换。

step 1 → 将你的 Data Model 转换成对应的 DataEntry 类别

蜡烛图的类别为 CandleChartDataEntry

而他有下列的建构子

x: Chart View 的 位置,但要注意的是,CandleStickChart 的 x 不建议使用 timeIntervalSince1970,建议使用 Charts Demo 的 index。因为在 render 的时候, Candle Stick Chart 是有他自己的逻辑。如果用 timeInterval ,我个人测试的结果,Candle 的烛身会画不出来。

open: 开市价格

shadowH: 当日最高点

shadowL: 当日最低点

close: 当日收盘价

所以,需要一个转换 StockKLine 到 CandleChartDataEntry 的 func

private func convert(stockStick: [StockKLine]) -> [CandleChartDataEntry] {
        
        var dataEntry = [CandleChartDataEntry]()
        
        for (i, each) in stockStick.enumerated() {
            
            let x = Double(i)
            if let open = each.open,
               let highest = each.highest,
               let lowest = each.lowest,
               let close = each.close {
                
                let candleData = CandleChartDataEntry(x: x, shadowH: highest, shadowL: lowest, open: open, close: close)
                dataEntry.append(candleData)
            }
        }
        
        return dataEntry
    }

step 2 → 将 DataEntry 类别转成 DataSet 类别

在 DataSet 是设定这一组数据画在 charts 上的设定,像是颜色、照哪一轴的比例。以

private func convert(dataEntry: [CandleChartDataEntry]) -> CandleChartDataSet {
        
        let dataSet = CandleChartDataSet(entries: dataEntry)
        
        dataSet.axisDependency = .left
        dataSet.setColor(.red)
        dataSet.drawIconsEnabled = false
        dataSet.shadowColor = .darkGray
        dataSet.shadowWidth = 0.5
        dataSet.decreasingColor = .systemGreen //注意下跌的颜色在该地区的习惯
        dataSet.decreasingFilled = true // 蜡烛线可以是实心,也可以是空心
        dataSet.increasingColor = .systemRed //注意下跌的颜色在该地区的习惯
        dataSet.increasingFilled = true
        dataSet.neutralColor = .black //当开盘 == 收盘的时候颜色
        
        return dataSet
    }

step 3 → 将 DataSet 转换成 Data,ChartView 中的 data 会依照这样的 Data 画。

private func convert(dataSet: CandleChartDataSet) -> CandleChartData {
        
        return CandleChartData(dataSet: dataSet)
    }

将 1、2、3 步骤连起来,就完成了

private func update(_ chartView: CandleStickChartView, stockStickList: [StockKLine]) {
        
        let dataEntry = convert(stockStick: stockStickList)
        let dataSet = convert(dataEntry: dataEntry)
        let data = convert(dataSet: dataSet)
        chartView.data = data
    }

整个 VC 的程序码如下

import UIKit
import Charts

class KLineViewController: UIViewController {
    
    @IBOutlet weak var chartContainer: UIView!
    
    private lazy var chartView: CandleStickChartView = {
        let view = CandleStickChartView()
        return view
    }()
    
    var kLineDataSet = [StockKLine]()

    // MARK: - life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBasicUI()
        setupCandleStickView()
        update(self.chartView, stockStickList: kLineDataSet)
    }
    
    // MARK: - private methods
    private func setupBasicUI() {
        
        chartContainer.backgroundColor = .clear
        
        chartContainer.addSubview(chartView)
        chartView.translatesAutoresizingMaskIntoConstraints = false
        
        chartView.leadingAnchor.constraint(equalTo: chartContainer.leadingAnchor).isActive = true
        chartView.topAnchor.constraint(equalTo: chartContainer.topAnchor).isActive = true
        chartView.trailingAnchor.constraint(equalTo: chartContainer.trailingAnchor).isActive = true
        chartView.bottomAnchor.constraint(equalTo: chartContainer.bottomAnchor).isActive = true
    }
    
     private func setupCandleStickView() {
        
        chartView.dragEnabled = false
        chartView.setScaleEnabled(true)
        chartView.maxVisibleCount = 200
        chartView.pinchZoomEnabled = true
        
        chartView.legend.enabled = false
        
        chartView.leftAxis.labelFont = UIFont.systemFont(ofSize: 10)
        chartView.leftAxis.spaceTop = 0.3
        chartView.leftAxis.spaceBottom = 0.3
        chartView.leftAxis.axisMinimum = 0
        
        chartView.rightAxis.enabled = false
        
        chartView.xAxis.labelPosition = .bottom
        chartView.xAxis.labelFont = UIFont.systemFont(ofSize: 10)
        
        chartView.maxVisibleCount = 20
    }
    
    private func convert(stockStick: [StockKLine]) -> [CandleChartDataEntry] {
        
        var dataEntry = [CandleChartDataEntry]()
        
        for (i, each) in stockStick.enumerated() {
            
            let x = Double(i)
            if let open = each.open,
               let highest = each.highest,
               let lowest = each.lowest,
               let close = each.close {
                
                let candleData = CandleChartDataEntry(x: x, shadowH: highest, shadowL: lowest, open: open, close: close)
                dataEntry.append(candleData)
            }
        }
        
        return dataEntry
    }
    
    private func convert(dataEntry: [CandleChartDataEntry]) -> CandleChartDataSet {
        
        let dataSet = CandleChartDataSet(entries: dataEntry)
        
        dataSet.axisDependency = .left
        dataSet.setColor(.red)
        dataSet.drawIconsEnabled = false
        dataSet.shadowColor = .darkGray
        dataSet.shadowWidth = 0.5
        dataSet.decreasingColor = .systemGreen
        dataSet.decreasingFilled = true
        dataSet.increasingColor = .systemRed
        dataSet.increasingFilled = true
        dataSet.neutralColor = .blue
        
        return dataSet
    }
    
    private func convert(dataSet: CandleChartDataSet) -> CandleChartData {
        
        return CandleChartData(dataSet: dataSet)
    }
    
    private func updateMaxMin(_ chartView: CandleStickChartView, dataSet: CandleChartDataSet) {
        
        let max = dataSet.yMax
        let min = dataSet.yMin
        chartView.leftAxis.axisMaximum = max * 1.05
        chartView.leftAxis.axisMinimum = min * 0.95
    }
    
    private func update(_ chartView: CandleStickChartView, stockStickList: [StockKLine]) {
        
        let dataEntry = convert(stockStick: stockStickList)
        let dataSet = convert(dataEntry: dataEntry)
        let data = convert(dataSet: dataSet)
        chartView.data = data
        
        updateMaxMin(chartView, dataSet: dataSet)
    }
}

完成上述步骤後,Candle Chart 成品如下

https://ithelp.ithome.com.tw/upload/images/20210924/20140622b20QKvBYlh.png

目前这张图,是没办法提供比较有效的资讯,最大原因,就是 x 轴并不是有效资讯。在第一步 Convert Data Entry 那边,我们要使用 index 来画 k 线,就丧失了日期这一讯息。

下一篇: 补上 x 轴遗失的资讯

下方是这次 D1 ~ D12 的完成品,可以下载来试
App Store - 台股申购日历

https://ithelp.ithome.com.tw/upload/images/20210924/20140622ypOBM0tgrZ.png


<<:  铁人赛 Day25 -- JavaScript 初体验(三) -- 建构子

>>:  D24-(9/24)-统一(1216)-刚开始学习买股票时就一直持有的股票

[Day13] Android - Kotlin笔记:Parcelable & Serializable 与 SafeArgs的传递

这边简单介绍两者差异和选择: Parcelable: 效能比Serializable好,在记忆体开销...

Day 25:从头开始的 Scroll Behavior

目前导览项目页面愈来愈完整,相对有愈来愈多小细节需要留意,尤其是资料量变多时,许多浏览时伴随的滚动效...

从听明牌,学习投资

获取明牌,并不一定就是赌徒心态;正确的观念是,应该是要先了解,人家何会推荐这只?是从基本面?消息面?...

Day 0x1 Intro & UVa10055 Hashmat the Brave Warrior

Intro UVa 一颗星选集 UVa Online Judge (wiki) 为线上自动评断系统,...

Day 05 : ML 专案生命周期

从无到有开发 ML 专案到布署需要 6 至 12 个月不等,在尚未有具体产出的过程中,会有对内部及...