iT邦幫忙

2021 iThome 鐵人賽

DAY 15
0
Mobile Development

使用 Swift 和公開資訊,打造投資理財的 Apps系列 第 16

D15 - 用 Swift 和公開資訊,打造投資理財的 Apps { 加權指數K線圖實作.3 - 使用 Charts 實作 K 線圖 }

  • 分享至 

  • xImage
  •  

如果你真的對畫圖很有興趣,而且很想自己做圖表的類別,那你可以使用程式在拿到資料後,用 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


上一篇
D14.5 - 用 Swift 和公開資訊,打造投資理財的 Apps { 來個中場回億番 }
下一篇
D16 - 用 Swift 和公開資訊,打造投資理財的 Apps { 加權指數 K 線圖實作.4 - 在 X 軸標上每一根 K 棒的日期 }
系列文
使用 Swift 和公開資訊,打造投資理財的 Apps37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言