8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DATUM STUDIOAdvent Calendar 2017

Day 25

ggplot2 + gridパッケージでpar(new=T)的に2軸プロットを作る

Last updated at Posted at 2017-12-25

この文章はDATUM STUDIO Advent Calendar 2017の25日目です。

はじめに

皆さんRのgridパッケージをご存知ですか?
gridExtra::grid.arrange()なら知っているけど…、という人が多いのではないでしょうか。
gridパッケージは何でも (!?) できます、しかし何をやるにも手間がかかります。
はっきり言って万人向けではありません。
ggplot2は、そのようなgridパッケージを扱いやすくラップしてくれているパッケージ、とも言えます。

ただし使いやすい反面、自由度に制限がかかっているのも事実です。
やりたいことができない!、という状況に出くわした際に、 
少しgridパッケージについて知っていれば、ggplot2パッケージの限界を突破することが可能です。

今回は、左右に異なるy軸を用いた2データのプロット (以下2軸プロット) を例に、
ggplot2 + gridパッケージの一端を紹介したいと思います。



ggplot2の2軸プロットの不便なところ

パッケージ作者のHadley氏は2軸プロットを推奨しておらず、ggplot2にはそのような機能が有りません。
version 2.2.0で右側y軸を作成することが可能となりましたが、
これはあくまで一つのデータに対し、左y軸はcm、右y軸はinch、といった用途を恐らく想定しており、
グラフの描写は左y軸を用いて行われます。

そのためggplot2のみで2軸プロットを行う場合には、右y軸データの左y軸へのリスケーリングが必要です。
(そのような方法は、nozma様のQiitaの記事 ggplot2で2軸プロットをする などで紹介されています)

しかしうっかりさんの私の様に、リスケーリングはやらかす可能性があるので嫌だ、という方は
そこそこいらっしゃるのではないでしょうか。

そこでgridパッケージ等を使い、baseパッケージのpar(new = T)的な感覚で
2軸グラフを作る方法を紹介します。



基本的な流れ

基本的な流れは、

  1. 2軸プロットで1つにまとめたいグラフ2枚を左y軸と同一の右y軸付きで作成し、
    ggplotGrob()でgrob (gtable)* 化
  2. 重ねる側のプロットからグラフ (点と線) および右y軸のgrobを抽出 (掘り出す)
  3. 重ねられる側の右y軸を消去 (見えなくする)
  4. 実際に重ねる

となります。
(* grob:グリッド・グラフィックスオブジェクト)
(gTreeはそれを束ねた物で、gtableはそれらをテーブル状に束ねた物 (略解))

それでは具体的にやっていきながら、ggplot2 x gridに親しんでいきましょう。




⓪ 例示用データの作成

適当にデータを二つ作ります。

# 以降で使用するパッケージの読み込み (tidyverse は dplyr & ggplot2でも可)
library(grid); library(gtable); library(tidyverse)

set.seed(111)
d1 <- data_frame(x = 2:8, y = sample(seq(110, 170, 10), 7))
d2 <- data_frame(x = 3:8, y = sample(3:8))

こんなデータです。

d1_x d1_y d2_x d2_y
2 150 NA NA
3 170 3 6
4 120 4 5
5 130 5 3
6 160 6 4
7 110 7 7
8 140 8 8



① まとめたいggplot2グラフ2枚の作成

まずはじめに、2軸プロットで1つにまとめたいグラフ2枚をggplot2で描きます。

意識すべき点は、

  • xlim()等でx軸の範囲を揃える
  • y軸の目盛り桁数の多い方を重ねられる側に用いる
  • ラベルタイトル等の情報は全て重ねられる側に持たせる
  • legendが欲しい場合、geom_blank(data = dummy_data)で重ねる側のlegend要素を作成
    (重ねる側プロットの色などは要手動指定)
  • 両グラフともscale_y_continuous(sec.axis = sec_axis(~ ., name = "2nd y title"))
    左y軸と同一の右y軸を作成する

y軸の目盛り桁数の多い方が重ねられる側のプロットとなるのは、完全にスペースの都合です。
右y軸以外の全パーツ (右y軸タイトル含む) は重ねられる側のプロットの物となるため、
それを意識して作図していきます。

ダミーデータによるlegendの作成は少々面倒に思えるかもしれませんが、
facet_wrap()での個別ylim等の指定でも使えるテクニックなので、覚えておいて損はありません。
ggplot2のデフォルト色については、hoxo_b様のggplot2のデフォルトの色を知りたい を参考にしてください。
facet_wrap()で個別軸範囲指定については、baptiste様のstack overflowの回答を参考にしてください。
  
事前にlegend作成の準備をしておきます。
重ねられる側のデータにlegend用の列を付け加え、
また重ねる側用のlegendを作るためにdummy_dataを作成します。
x値が揃っていない場合、合計範囲も算出しておきましょう。

d1 <- d1 %>% mutate(col = "d1 (left y axis)")
dummy_data <- data_frame(x = mean(d1$x), y = mean(d1$y), col = "d2 (rignt y axis)")

x_range <- c(d1$x, d2$x) %>% range()

それではggplot2で作図します。

base_plot <- ggplot(d1, aes(x, y, colour = col)) + 
  geom_line() + 
  geom_point() + 
  scale_y_continuous(sec.axis = sec_axis(~ ., name = "y axis (right)")) + # 右y軸
  xlim(x_range) +                # x軸の範囲を揃える
  xlab("x axis") +
  ylab("y axis (left)") + 
  ggtitle("ggplot2 x grid") +
  geom_blank(data = dummy_data)  # geom_blank(ダミーデータ)でlegend要素を加える

additional_plot <- ggplot(d2, aes(x, y)) + 
  geom_line(colour = "#00BFC4") +              # デフォルト色 (n = 2) の2色目を指定
  geom_point(colour = "#00BFC4") +
  scale_y_continuous(sec.axis = sec_axis(~ ., name = "not used")) +     # 右y軸
  xlim(x_range) +                           # x軸の範囲を揃える
  xlab("not used") +                        # 不要行です
  ylab("not used") +                        # 不要行です
  ggtitle("(additional_plot; not used)")    # 不要行です

## (参考用)
# ちなみにadditionalの方を
# scale_y_continuous(sec.axis = sec_axis(~ ., name = "not used", lim = c(3, 9))
# とすると、重ねた際に右y軸と罫線が揃って見栄えがよくなります。
# with(d1, labeling::extended(min(y), max(y), m = 5)) %>% length() # 7
# with(d2, labeling::extended(min(y), max(y), m = 5)) %>% length() # 6 不揃い
# with(d2, labeling::extended(min(y), 9, m = 5)) %>% length() # 7 揃った

ggplotGrob()でgrob (gtable) 化しておきましょう。

base_gtable <- ggplotGrob(base_plot)
additional_gtable <- ggplotGrob(additional_plot)

ggplot2の段階ではこのようなグラフです。
fig2


② 重ねる側のgtableからグラフ(点と線)のgrobと右y軸のabsoluteGrobを抜き出す

慣れていない人にとっての最難関ポイントです。
gtableはかなり深いlist構造からなっており、目的のgrobやgTreeを得るには少しコツが入ります。
迷ったら、names()を実行するとヒントが得られます。
(が、facet_wrap()などで構造が変わらない限り位置は共通なので、実は真面目に掘り出す必要はありません)

gtableの構造を見てみましょう。

additional_gtable
z cells name grob
1 0 ( 1-10, 1- 7) background rect[plot.background..rect.9420]
2 5 ( 5- 5, 3- 3) spacer zeroGrob[NULL]
3 7 ( 6- 6, 3- 3) axis-l absoluteGrob[GRID.absoluteGrob.9406]
4 3 ( 7- 7, 3- 3) spacer zeroGrob[NULL]
5 6 ( 5- 5, 4- 4) axis-t zeroGrob[NULL]
6 1 ( 6- 6, 4- 4) panel gTree[panel-1.gTree.9383]
7 9 ( 7- 7, 4- 4) axis-b absoluteGrob[GRID.absoluteGrob.9399]
8 4 ( 5- 5, 5- 5) spacer zeroGrob[NULL]
9 8 ( 6- 6, 5- 5) axis-r absoluteGrob[GRID.absoluteGrob.9413]
10 2 ( 7- 7, 5- 5) spacer zeroGrob[NULL]
11 10 ( 4- 4, 4- 4) xlab-t zeroGrob[NULL]
12 11 ( 8- 8, 4- 4) xlab-b titleGrob[axis.title.x..titleGrob.9386]
13 12 ( 6- 6, 2- 2) ylab-l titleGrob[axis.title.y..titleGrob.9389]
14 13 ( 6- 6, 6- 6) ylab-r titleGrob[axis.title.y.right..titleGrob.9392]
15 14 ( 3- 3, 4- 4) subtitle zeroGrob[plot.subtitle..zeroGrob.9417]
16 15 ( 2- 2, 4- 4) title titleGrob[plot.title..titleGrob.9416]
17 16 ( 9- 9, 4- 4) caption zeroGrob[plot.caption..zeroGrob.9418]

panelがメインのグラフ部分 (背景なども含む)、axis-rが右側y軸となります。
cellの番地は重ね書きで必要となるので、メモしておきましょう (パネルはc(6, 4), 右y軸はc(6, 5)です)

gtable名$grobs[[上表左のindex番号]]でgrob列にあるオブジェクトが抜き出せます。
右y軸については上表のabsoluteGrob[GRID.absoluteGrob.7024]を抜き出すだけでいいのですが、
グラフ (点と線) では、panelのgTree (上表gTree[panel-1.gTree.6994]) では背景まで付いてくるため、
さらに掘って、その中にある点grobと線grobを抜き出す必要があります。

それではまず始めにグラフ (点と線) を抜き出します。
gTreeの中身はchildrenに収まっています。

additional_gtable$grobs[[6]]$children

## (gTree[grill.gTree.98], zeroGrob[NULL], polyline[GRID.polyline.83], points[geom_point.points.85], zeroGrob[NULL], zeroGrob[panel.border..zeroGrob.86])

今回用があるのはchildrenの3番目と4番目の
polyline[GRID.polyline.6922]points[geom_point.points.6924]です。

additional_line_and_points <- additional_gtable$grobs[[6]]$children[3:4] 

# 以下のようなコードを用いると、添え字の指定なしで抜き出せます
# additional_grobs$grobs[[6]]$children %>% 
#    purrr::discard(str_detect(names(.), "Tree|NULL|zero"))

右y軸はもっと簡単に抜き出せます。

additional_right_y_axis <- additional_gtable$grobs[[9]]



③ 重ねられる側の右y軸を消去 (見えなくする)

次に、重ねられる側の右y軸を見えなくします。
右y軸のabsoluteGrobのパラメータにalpha = 0を追加することでこれを実現します。

Grob系オブジェは色といった作図パラメータをgpというリスト内に持っており、
gpar()関数に指定したいパラメータを与えて、これに突っ込むことで書き換えができます。

base_gtable$grobs[[9]]$gp <- gpar(alpha = 0)    # 右y軸のindexは先ほど同様9です。

これで、重ねられる側の右y軸が見えなくなりました (完全透過)。



④ 抜き出した点・線・右y軸を重ねる

あとは重ねるだけです。
gtable_add_grob()を用いて重ねるのですが、この際に上のgtableのcell番地が必要になります。
(パネルはc(6, 4), 右y軸はc(6, 5)です)
同セルに複数のgrobを重ね書きする場合、nameを明示する必要がある点 (重複回避のため) に注意が必要です。
(additional_line_and_points は二つのgrobからなるため、これに該当します)

base_and_additional_grobs <- base_gtable %>% 
  gtable_add_grob(additional_line_and_points, 6, 4, name = c("a", "b")) %>% 
  gtable_add_grob(additional_right_y_axis, 6, 5)

完成!!

grid.newpage()
grid.draw(base_and_additional_grobs)

解説しながら行ったため作業量が多いように感じたと思いますが、
慣れれば、ぱぱっと作れるようになります (!?)

Let's enjoy !!

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?