📊
Swift ChartsでGitHubのLanguagesのグラフを作成
概要
- 下記のようにGitHubのLanguagesのグラフを、Swift Chartsで再現してみます。
GitHubの表示 | Swift Chatrsで作成したもの |
---|---|
GitHub
実装
GitHubの言語別の色の取得
- GitHubで使われている言語別の色の定義を行います。
- 今回は下記の設定をおかりしました。
-
GitHubLanuageColor
というクラスを作り、GitHubLanguageColor.shared.getColor(withName: name)
のように言語名からColorを取得ようにしています。-
hex文字列 -> Color
の変換に関してはUse Hex color in SwiftUIを参考にしました
-
GitHubLanguageColor.swift
struct GitHubLanguageColor {
// MARK: - Properties
static let shared: GitHubLanguageColor = .init()
private let languages: [Language]
// MARK: - LifeCycle
private init() {
// 定義ファイルの読み込み
// (省略)
}
// MARK: - Public methods
func getColor(withName name: String?) -> Color? {
guard let name else {
return nil
}
let language = languages.first { language in
name == language.name
}
return language?.color
}
}
リポジトリで使用されている言語情報
情報の取得方法
- リポジトリで使用されている言語情報は下記のGitHub APIで取得できます。
https://api.github.com/repos/<user-name>/<repository-name>/languages
- 今回表示の例としては下記のリポジトリを使っています。
- よって言語情報を取得するためのURLは
https://api.github.com/repos/robovm/apple-ios-samples/languages
となり、下記の情報が取得できます。- valueの算出方法はわかりませんが、この値を言語の使用量として扱えそうですね。
{
"Objective-C": 11414333,
"C++": 2479029,
"Swift": 2299854,
"Objective-C++": 1217905,
"C": 794349,
"Metal": 88350,
"JavaScript": 78308,
"HTML": 38193,
"Rebol": 12190,
"Python": 10621,
"Ruby": 5325,
"GLSL": 2584
}
言語情報のstructの作成
- 上記の情報をjsonとして読み込み、下記のstructとして言語別に情報を保存します。
struct Language: Identifiable {
let name: String
let amount: Int
let percentage: Double
let color: Color
var titleForLegend: String {
"\(name) \((percentage * 100).truncate(places: 1))%"
}
var id: String { name }
}
- 今回Swift Chartsでは%で扱いたいので、下記のように一回amountの総量を計算して%を算出します。
let sumAmount: Double = json.reduce(0.0) { partialResult, dict in
let amount = dict.value
return partialResult + Double(amount)
}
...
let percentage = Double(amount) / sumAmount
- またGitHubの例を見る通り、%の小さいものは
Other
にまとめられているので、それに倣っています。
- コード全体としては以下の通りになります
Language+createSampleData.swift
extension Language {
private static let otherThresholdPercentage = 0.004 // 0.4%未満をOtherとする
static func createSampleData() -> [Language] {
// ref: https://api.github.com/repos/robovm/apple-ios-samples/languages
guard
let url = Bundle.main.url(forResource: "sample-repository-languages", withExtension: "json"),
let data = try? Data(contentsOf: url),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Int]
else {
fatalError("sample-repository-languages.jsonの読み込みに失敗しました。")
}
let sumAmount: Double = json.reduce(0.0) { partialResult, dict in
let amount = dict.value
return partialResult + Double(amount)
}
var languages = [Language]()
var otherAmount = 0
var otherPercentage: Double = .zero
for (name, amount) in json {
let percentage = Double(amount) / sumAmount
if percentage < otherThresholdPercentage {
// 一定値以下のものをOtherにまとめる
otherAmount += amount
otherPercentage += percentage
continue
}
languages.append(
Language(name: name,
amount: amount,
percentage: percentage,
color: GitHubLanguageColor.shared.getColor(withName: name) ?? .accentColor)
)
}
languages.sort(by: { first, second in
// 使用率が大きい順にソート
first.amount > second.amount
})
if otherPercentage > 0 {
languages.append(
Language(name: "Other",
amount: otherAmount,
percentage: max(0.001, otherPercentage),
color: .otherLanguage)
)
}
return languages
}
}
グラフの作成
基本
- 今回使うグラフは下図のような積み上げ棒グラフで
BarMark
を使用します。
- とりあえず前述データを表示をすると以下のようになります。
-
Array(languages.enumerated())
に関しては後述のpaddingで使うためこうしています。 - 必要がなければ通常通り
ForEach(languages)
でOKです。
-
Chart {
ForEach(Array(languages.enumerated()), id: \.element.name) { index, language in
BarMark(
x: .value("Percentage", language.percentage)
)
.foregroundStyle(language.color)
}
}
体裁の調整
-
Chart
のmodifierを使って体裁を整えていきます。 -
chartPlotStyle
でグラフの表示領域を設定し、両端に丸みをもたせます - また今回x軸は不要なので消しています。
Chart {
...
}
.chartPlotStyle { plotArea in
plotArea
.frame(height: 10)
.cornerRadius(5)
}
.chartXAxis(.hidden)
凡例の追加
- 凡例を追加します。今回テキストと色を指定したいのでカスタムで設定する必要があります。
-
chartForegroundStyleScale(_:)の引数が
KeyValuePairs<DataValue, S>
であり、これを動的に作るのは難しそうでした。 - そのため`chartForegroundStyleScale(domain:type:)を使っています。
- これにより凡例の文字列のカスタムまで行っています。
.chartForegroundStyleScale(
domain: languages.map { $0.titleForLegend },
range: languages.map { $0.color }
)
paddingの追加
- 最後に要素の間にpaddingを追加します。
- (ここの方法が調べても出てこなかったので、トリッキーな自己流の実装となっています。他にいい方法があれば教えて下さい…!)
- 発想としてはSwift Charts: Raise the barであった下記の
RectangleMark
を使います。 - paddingを擬似的に表現するため、要素の間に
RectangleMark
を配置します。
- 下記のようにForEachの中に
RectangleMark
を追加しています。 - 最後の要素の後だけはpaddingがいらないので、
.foregroundStyle
で.clear
としています。
Chart {
ForEach(Array(languages.enumerated()), id: \.element.name) { index, language in
BarMark(
x: .value("Percentage", language.percentage)
)
.foregroundStyle(language.color)
RectangleMark(
x: .value("padding", calculatePaddingXPosition(at: index)),
width: 4
)
.foregroundStyle(
(index == languages.count - 1) ? .clear : .otherLanguage
)
}
}
- また
RectangleMark
の位置は地道に計算しています。
private func calculatePaddingXPosition(at index: Int) -> Double {
if index == languages.count - 1 {
return .zero
}
var xPosition: Double = .zero
for markIndex in 0...index {
xPosition += languages[markIndex].percentage
}
return xPosition
}
- 以上で完成です!
- 余談ながらpaddingとOtherの色を淡いグレーで合わせているのがミソだったりします。
- もしpaddingの色を背景色(
Color(uiColor: .systemBackground)
)にした場合、下記のようにOtherの表示が乱れる結果となります。
Discussion