ggplot2の棒チャートでy軸のデータに時刻形式の文字列を使う

久々に更新します。

Rのチャート生成用のパッケージのggplot2ものすごく便利なので、Rをインストールしてすぐ使い始めましたが全然使いこなせていません。

今回も棒チャートのy軸に時間データを使う必要が出てきてしまったのですが、対応方法がわからず詰んでしまっていました。
ただ、日曜日の午後を使い倒した甲斐もあり無理やりですが対応できたため、qiitaに残しておきます。

長々と続くので、結果はリンクからどうぞ。
[成功例](#成功例)

## 前提

下記のような時間が記録されたデータがあります。  
名前をx軸、記録をy軸にした棒チャートを作成しなければいけませんでした。

| 名前 | 記録 |
| ---- | ---- |
| A | 00:14:01 |
| B | 00:15:32 |
| C | 00:18:02 |
| D | 00:00:00 |

また、下記のような縛りもあります。

- y軸のメジャーは 00:00:00 から表示すること。
- ggplot2の初期設定通りプロットエリアの内側には余白を作ること

## 失敗例1. 文字列をそのまま使う

最初に下記のコードを試しました。  
記録の時間形式の文字列をそのまま使っています。

```r
library('ggplot2')

# 本来はcsv等から読み込みますが、試しなのでハードコーディングします。
chart_data = data.frame(
    x = c('A', 'B', 'C', 'D'),
    y = c('00:14:01', '00:15:32', '00:18:02', '00:00:00')
)

# 棒チャートを生成
plot = ggplot(chart_data, aes(x=x, y=y))
plot = plot + geom_bar(stat='identity')
```

すると、下記のような画像が。

![失敗した画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194644/672dace2-6500-108d-7b3f-2a84d376f759.png)


数値としては読み込んでくれているのか各要素それぞれの高さに差はありますが、y軸のメジャーが00:00:00から始まっておらず、要件を満たせていません。


## 失敗例2. 文字列で下限値を設定する

1回目は数値の差がわかる範囲に絞らられていたため、00:00:00から始まっていなかったものと思います。そのため、下限値を設定すれば数値全体を表示できると考え、下記のコードを試しました。13〜14行目追記しています。

```r
library('ggplot2')

# 本来はcsv等から読み込みますが、試しなのでハードコーディングします。
chart_data = data.frame(
    x = c('A', 'B', 'C', 'D'),
    y = c('00:14:01', '00:15:32', '00:18:02', '00:00:00')
)

# 棒チャートを生成
plot = ggplot(chart_data, aes(x=x, y=y))
plot = plot + geom_bar(stat='identity')

# 下限値を00:00:00にする
plot = plot + ylim('00:00:00', NA)
```

すると、D以外のチャートが表示されなくなりました。

![D以外のチャートが表示されなくなった例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194644/2aa9150e-f16d-fb91-0602-65bca945e52f.png)


このコードを実行した時、下記メッセージが出ていました。  
数値がない３行分は省いたよ。といった意味合いだと思うのですが、数値はあるのに解せません・・。

```
警告メッセージ: 
Removed 3 rows containing missing values (position_stack). 
```

予想ですが、文字列を正しく数値に変換できなかったのだと思います。  
もしくは、「'00:00:00' = 0」であることを認識できておらず、値の比較ができなかったため省いた、とかでしょうか・・。

## 失敗例3. 値を日時型に変換する

これまでは文字列をそのまま使っていたから正しく読み込めなかったのではないかと予想しました。
一度日時型に変換すれば正しく表示できるのではないかと考え、下記のコードを試しました。

```r
library('ggplot2')

# 本来はcsv等から読み込みますが、試しなのでハードコーディングします。
chart_data = data.frame(
    x = c('A', 'B', 'C', 'D'),
    y = c('00:14:01', '00:15:32', '00:18:02', '00:00:00')
)

# POSIXct型に変換する
chart_data$y = as.POSIXct(chart_data$y, format='%H:%M:%S', tz='Asia/Tokyo')

# 棒チャートを生成
plot = ggplot(chart_data, aes(x=x, y=y))
plot = plot + geom_bar(stat='identity')
```

Rの日時型はPOSIXct型、というもので表すようです。as.POSIXctを使って変換を行うのですが、変換したデータを使ってプロットすると枠いっぱいまで伸びたチャートが生成されていまいました。

![枠いっぱいになってしまった例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194644/23f4415e-acee-1d6b-f3aa-d71537fe42f3.png)


また予想ですが、時間ではなく、日付での比較しかできなくなってしまい、それぞれの要素の高さも同じになってしまったのだと考えます。

## 失敗例4. 方法3に下限値を設定

ひとまず、表示範囲を0〜にするため、下限値を設定します。
POSIXct型の00:00:00が必要だったため、型変換した値を下限値に設定しました。

```r
library('ggplot2')

# 本来はcsv等から読み込みますが、試しなのでハードコーディングします。
chart_data = data.frame(
    x = c('A', 'B', 'C', 'D'),
    y = c('00:14:01', '00:15:32', '00:18:02', '00:00:00')
)

# POSIXct型に変換する
chart_data$y = as.POSIXct(chart_data$y, format='%H:%M:%S', tz='Asia/Tokyo')

# 棒チャートを生成
plot = ggplot(chart_data, aes(x=x, y=y))
plot = plot + geom_bar(stat='identity')

# 当日00:00:00のPOSIXctデータを下限値、上限値を設定なしにする
plot = plot + ylim(as.POSIXct('00:00:00', format='%H:%M:%S', tz='Asia/Tokyo'), NA)
```

すると、y軸のメジャーはちょうど良い数値になりましたが、全てのチャートが非表示になってしまいました。

![チャートが消えた例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194644/1aedbed8-b35b-26d4-0b92-b65e3885565e.png)


## 成功例

ここまできて、日時のままチャートを作ってもうまくいかないと思い、、秒数をy軸の値として使い、y軸のメジャーを時刻形式に置き換えてしまおうと考えました。

```r
library('ggplot2')

# 本来はcsv等から読み込みますが、試しなのでハードコーディングします。
chart_data = data.frame(
    x = c('A', 'B', 'C', 'D'),
    y = c('00:14:01', '00:15:32', '00:18:02', '00:00:00')
)

# POSIXct型に変換する。当日の00:00:00からの経過秒数をy軸の数値とする
y_data = as.POSIXct(chart_data$y, format='%H:%M:%S', tz='Asia/Tokyo')
today = as.POSIXct(format(Sys.Date(), '%Y-%m-%d %H:%M:%S', tz='Asia/Tokyo'))
# 経過秒数をy軸の数値とする
chart_data$y = as.numeric(difftime(y_data, today, units='secs'))

# 棒チャートを生成
plot = ggplot(chart_data, aes(x=x, y=y))
plot = plot + geom_bar(stat='identity')

# y軸に表示されるメジャーの数値を取得する
y_panel_value = ggplot_build(plot)$layout$panel_params[[1]]$y.major_source
# POSIXct型に変換
y_panel_value_POSIXct = as.POSIXct(
    y_panel_value,
    format='%H:%M:%S',
    tz='Asia/Tokyo',
    origin=today
)

# y軸のbreaksとlabelsの数を合わせるために両方を指定する。
# breaksは取得したメジャー数値
# labelsはPOSIXct型に変換し、さらにフォーマットを変更した数値
plot = plot + scale_y_continuous(
    breaks=y_panel_value,
    labels=format(y_panel_value_POSIXct, format='%H:%M:%S', tz='Asia/Tokyo')
)
```

1つ目のポイントは11行目と、14行目です。
11行目で、y軸の値をPOSIXct型に変換して、当日の00:00:00からの経過時間にとして扱えるようにしました。
14行目で、当日の00:00:00との差分を取得して、y軸の値としています。

POSIXctは内部的にはUNIX時間の経過秒数を持っているので、差分をとれば秒数のみを取得できます。

2つめは21〜28行目です。
21行目で、ggplot_build関数で、実際にplotされる際のy軸のメジャーを取得しています。
y軸に秒数のみを使用しているため、取得したメジャーも数値型になってしまっています。これを時刻形式に書き換えられるようにするため、POSIXct型に変換した値を別の変数に定義します。

最後に33行目で、y軸の表示形式を書き換えます。

コードについて長々と記載しましたが、出力結果は下記の通りです。

![成功例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194644/3454b36c-1c5e-feed-4ef0-d7a720fa9bed.png)

データラベルをつけるとこのようになります。

![成功例2ラベル追加](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194644/e7c0445b-de29-6ac9-f9a1-fcb806c94f3c.png)

[成功例](#成功例)のコードに下記のコードを追記すれば追加できます。  
ここでは表示形式を書き換えないと日付と時刻が表示されてしまうため注意が必要です。

```r
plot = plot + geom_text(
    label=format(y_data, format='%H:%M:%S', tz='Asia/Tokyo'),
    vjust=-0.5
)
```

## おわりに

ggplot2、自由度が比較的高いようで、なかなか仕様を覚えられず苦労しております。  
また、日本語で記載された情報も比較的少なく、一つ詰まってしまうと、なかなか抜け出せません。英語、読めるようにならねければ・・です。

また、ご覧いただいた方の中で、もっと適切な方法があるなど、ご存知の方がいらっしゃいましたらご教示いただけますと幸いでございます。

よろしくお願いいたします。