今度は外部ライブラリChartsを利用して、棒グラフを作成してみる。

目標

  1. 値が最大のデータは色をオレンジにする
  2. アニメーションがある
  3. 棒グラフの上に値を表示する
  4. ページ切り替えができる棒グラフを作る
  5. タップしたらイベントを発生させる

1〜3、5は機能としてある。4だけ頑張って作る。思い通りのレイアウトにするためにはプロパティとかドキュメントとかを漁る必要があるが、どこにどのプロパティがあるのかは大体予想できる。

  1. ChartDataSet.colorsで各棒の色を変更できる。
  2. BarChartView.animate(yAxisDuration:)を利用。
  3. BarChartView.drawValueAboveBarEnabled = trueとする。表示形式を変更するためにはChartDataSet.valueFormatterにフォーマット用のオブジェクトを指定する。
  4. ScrollViewの中ににBarChartViewを複数配置。
  5. ChartViewDelegateを利用。

その他デフォルトの設定だと表示する情報量が多すぎるので、いくつかのプロパティをいじる。

Chartsのインストール

まず、CocoaPodsがインストールされていることが前提。

プロジェクトフォルダで以下のコマンドを実行。

1
$ pod init

podfileが作成されるので、それを編集する。use_frameworks!の下に以下の記述を追加。

1
pod 'Charts'

プロジェクトフォルダで以下のコマンドを実行。

1
$ pod install

以降、プロジェクトはプロジェクト名.xcodeprojではなくプロジェクト名.xcworkspaceから開く。

基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import UIKit
import Charts

struct BarChartModel {
    let value: Int
    let name: String
}

class ViewController: UIViewController {
    
    let barItems = [
        (7, "太郎"), (1, "次郎"), (2, "三郎"), (6, "四郎"), (3, "五郎"),
        (9, "六郎"), (2, "七郎"), (3, "八郎"), (1, "九郎"), (5, "十郎"),
        (1, "十一郎"), (1, "十二郎"), (6, "十三郎")
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let barChartView = createBarChartView()
        self.view.addSubview(barChartView)
        barChartView.translatesAutoresizingMaskIntoConstraints = false
        barChartView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 80).isActive = true
        barChartView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -80).isActive = true
        barChartView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        barChartView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
    }
    
    private func createBarChartView() -> BarChartView {
        let barChartView = BarChartView()
        barChartView.data = createBarChartData(of: barItems.map({BarChartModel(value: $0.0, name: $0.1)}))
        return barChartView
    }
    
    private func createBarChartData(of items: [BarChartModel]) -> BarChartData {
        let entries: [BarChartDataEntry] = items.enumerated().map {
            let (i, item) = $0
            return BarChartDataEntry(x: Double(i), y: Double(item.value))
        }
        let barChartDataSet = BarChartDataSet(entries: entries, label: "Label")
        let barChartData = BarChartData(dataSet: barChartDataSet)
        return barChartData
    }
}

これだけの記述で以下の棒グラフが描ける。

説明

棒グラフに限っては、以下の手順で作る。

  • BarChartEntryにデータを詰める
  • BarChartEntryの配列からBarChartDataSetを作成
  • BarChartDataSetからBarChartDataを作成
  • BarChartViewBarChartDataを詰める

色をつける

ViewControllerに最大値のプロパティを持たせる。

1
lazy var maxVal: Int = barItems.map({ $0.0 }).max()!

barChartDataSetに色の配列をセットする。

1
barChartDataSet.colors = items.map { $0.value == maxVal ? .systemOrange : .systemBlue }

アニメーション

createBarChartView関数に以下の記述を追加。

1
barChartView.animate(yAxisDuration: 1)

棒グラフの下に名前を表示する

これは、棒グラフのx軸を設定することで実現できる。

createBarChartView関数に以下の記述を追加。

1
2
3
barChartView.xAxis.labelCount = items.count
barChartView.xAxis.labelPosition = .bottom
barChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: items.map({$0.name}))
  • labelCountを設定しておかないと、ラベルの表示が奇数番目のみにになるので注意。
  • labelPositionを設定しておかないと、ラベルの位置が上になるので注意。
  • valueFormatterには、軸の表示方法を管理するオブジェクトを定義する。

IndexAxisValueFormatterの代わりに、IAxisValueFormatterに準拠したオブジェクトを指定すると、x軸の書式をカスタマイズできる。

例えば、以下のようにXAxisFormatterを定義すると、これはIndexAxisValueFormatterと同じような振る舞いをする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class XAxisFormatter: IAxisValueFormatter {
    let items: [BarChartModel]
    init(of items: [BarChartModel]) {
        self.items = items
    }
    func stringForValue(_ value: Double, axis: AxisBase?) -> String {
        let index = Int(value)
        return self.items[index].name
    }
}

棒グラフの上にある数字の書式設定

createBarChartData関数に以下の記述を追加。

1
2
barChartView.valueFont = .systemFont(ofSize: 20)
barChartDataSet.valueFormatter = ValueFormatter(of: items)

ValueFormatterを定義する。これはIValueFormatterに準拠したクラス。このstringForValueで、x軸の値valueに対するラベルの値を返すように設定する。

1
2
3
4
5
6
7
8
9
class ValueFormatter: IValueFormatter {
    let items: [BarChartModel]
    init(of items: [BarChartModel]) {
        self.items = items
    }
    func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
        return "\(Int(value))"
    }
}

細かい設定

グリッドとかy軸とかはいらないので、それを消す設定をする。

createBarChartView関数に以下の記述を追加。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// グリッドやy軸を非表示
barChartView.xAxis.drawGridLinesEnabled = false
barChartView.leftAxis.enabled = false
barChartView.rightAxis.enabled = false

// 凡例を非表示にする
barChartView.legend.enabled = false

// ズームできないようにする
barChartView.pinchZoomEnabled = false
barChartView.doubleTapToZoomEnabled = false

ページ分け

1ページ内に13本のグラフが並んでいるのは見づらい。なのでScrollViewを使ってページ分けする。

Main.storyboard

前回通りにやる。

ViewController.swift

viewDidLoadでは主にscrollViewの設定をする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
override func viewDidLoad() {
    super.viewDidLoad()
    
    scrollView.frame = CGRect(
        x: 0,
        y: 0,
        width: scrollView.superview!.frame.width,
        height: scrollView.superview!.frame.height
    )
    let contentsView = createContentsView(
        of: barItems.map({ BarChartModel(value: $0.0, name: $0.1 ) }),
        barsCountPerPage: 5
    )
    scrollView.addSubview(contentsView)
    scrollView.contentSize = contentsView.frame.size
    scrollView.isPagingEnabled = true
}

createContentsViewを定義する。こちらもやってること自体は前回とあまり変わっていない。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private func createContentsView(of items: [BarChartModel], barsCountPerPage: Int) -> UIView {
    let itemsPerPage = stride(from: 0, to: items.count, by: barsCountPerPage).map {
        Array(items[$0 ..< min($0 + barsCountPerPage, items.count)])
    }
    let contentsView = UIView(frame: CGRect(
        x: 0,
        y: 0,
        width: scrollView.frame.width * CGFloat(itemsPerPage.count),
        height: scrollView.frame.height
    ))
    for (i, items) in itemsPerPage.enumerated() {
        let barChartView = createBarChartView(of: items)
        let percent = CGFloat(items.count) / CGFloat(itemsPerPage[0].count)
        barChartView.frame = CGRect(
            x: scrollView.frame.width * CGFloat(i),
            y: 0,
            width: scrollView.frame.width * percent,
            height: scrollView.frame.height
        )
        contentsView.addSubview(barChartView)
    }
    return contentsView
}

createBarChartViewに以下の記述を追加。これで全ページで同じ縮尺になる。

1
barChartView.leftAxis.axisMaximum = Double(maxVal) + 1

タップイベント

createBarChartViewに以下の記述を追加。

1
barChartView.delegate = self

ViewControllerextensionを追加する。chartValueSelectedメソッドでタップ時の処理を指定する。例えば次のようにすると、棒グラフの名前と値を取得できる。

1
2
3
4
5
6
7
extension ViewController: ChartViewDelegate {
    func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
        let axisFormatter = chartView.xAxis.valueFormatter!
        let label = axisFormatter.stringForValue(entry.x, axis: nil)
        print(label, entry.y)
    }
}

ちなみに、先ほどValueFormatteritemsを持たせていたため、次のようにしてitemの値を取得することも可能(あまり綺麗な方法ではないが)。

1
2
3
4
5
6
7
8
extension ViewController: ChartViewDelegate {
    func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
        let valueFormatter = chartView.data?.dataSets[0].valueFormatter as! ValueFormatter
        let items = valueFormatter.items
        let index = Int(entry.x)
        print(items[index])
    }
}

参考