ggplot2の拡張パッケージをつくるのにggplot_add()が便利そう

ggplot2の次期バージョンにggplot_add()という総称関数が入りました。

これによって、通常のgeomやstatよりも柔軟なggplot2の拡張パッケージをつくれる可能性があります。

geomやstatの制限

大概のggplot2拡張パッケージは、以下のドキュメントに従ってggprotoを継承したクラスをつくるだけで事足ります。ggprotoはggplot2のほぼすべての要素が使うフレームワークであり、このレールに乗っておけばまあ間違いない、というものです。

しかし、お膳立てされたフレームワークであるがゆえに制限もあります。私も完全には理解できてないんですが、例えば、gghighlightパッケージをつくるときにつまづいたのは、geom/statはaesマッピング済みのデータしか受け取れないということです。

例えば以下のようなデータがあるとします。

moto_no_data <- data.frame(
  oops   = 1:10,
  alas   = 1:10,
  sugosa = 1:10
)

ここで、引数の表現がTRUEになる要素だけ目立たせるGeomSugoiというgeomと、それに対応するgeom_sugoi()という関数をつくることを考えてみましょう。コードはこんな感じになるでしょう。

library(ggplot2)

ggplot(moto_no_data, aes(x = oops, y = alas)) +
  geom_sugoi(expr = sugosa > 5)

ここで、geom_sugoi()の中で直接sugosa > 5を評価することはできません。データやマッピングggplot(...)の中に定義されていますが、ggplot(...)geom_sugoi(...)は以下のような関係であり、geom_sugoi(...)から直接ggplot(...)の中を覗くことはできません*1

`+`(ggplot(...), geom_sugoi(...))

余談ですが、もしggplot2が+ではなく%>%を使う設計になっていれば、

ggplot(...) %>%
  geom_sugoi(...)

は以下のように評価されるのでgeom_sugoi(...)からggplot(...)の中にアクセスすることができたはずです。惜しい...

geom_sugoi(ggplot(...), ...)

さて、そんなわけで、geom_sugoi()にできるのは、sugoi > 5という表現式を評価せずとりあえずGeomSugoiの属性に突っ込み、あとでプロットを組み立てるフェーズに望みをかけることです。

ですが、これもうまくいきません。GeomSugoiは元データを知りえないからです。ドキュメントに載っている簡単な例をチラ見すると、

StatChull <- ggproto("StatChull", Stat,
  compute_group = function(data, scales) {
    data[chull(data$x, data$y), , drop = FALSE]
  },

  required_aes = c("x", "y")
)

となっています。data$xdata$yというように列名が決め打ちになっていることからも分かるように、compute_group()に渡ってくるデータは元のこういうデータではなく、

#>    oops alas sugosa
#> 1     1    1      1
#> 2     2    2      2
#> 3     3    3      3
#> 4     4    4      4
#> 5     5    5      5
#> 6     6    6      6
#> 7     7    7      7
#> 8     8    8      8
#> 9     9    9      9
#> 10   10   10     10

aes()によるマッピングを反映済みのこういうデータです。

moto_no_data
#>     x  y
#> 1   1  1
#> 2   2  2
#> 3   3  3
#> 4   4  4
#> 5   5  5
#> 6   6  6
#> 7   7  7
#> 8   8  8
#> 9   9  9
#> 10 10 10

元の列名は残っていないし、マッピングがない列(この例で言うとsugoi)は省かれてしまっています。これではsugosa > 5を評価することができません。

+.ggを上書きする?

絶望的な気持ちになりますが、実は方法がないこともないです。上に書いたこれを思い出してください。

`+`(ggplot(...), geom_sugoi(...))

+、つまりggplotオブジェクトに対して呼び出される+.ggは、ggplot(...)にもgeom_sugoi(...)にもアクセスできる位置にあります。なので、これを上書きして、「第二引数がgeom_sugoi(...)の時は、ggplot(...)の中の元データを取り出してgeom_sugoi(...)の中の表現式を評価する」という処理を実装する(そしてもともと+.ggがやっていたことをコピペする)というダーティーな方法があります。

しかし、安心してください、ggplot_add()の登場によってこんなダーティーなことをしなくてもよくなりました。

ggplot_add()

ggplot_add()は、+.ggの中で呼び出される総称関数です。

現時点での+.ggのコードは以下のようになっています。分かりづらいんですが、add_ggplot()ggplot_add()という関数に注目してください。

"+.gg" <- function(e1, e2) {
  # Get the name of what was passed in as e2, and pass along so that it
  # can be displayed in error messages
  e2name <- deparse(substitute(e2))

  if      (is.theme(e1))  add_theme(e1, e2, e2name)
  else if (is.ggplot(e1)) add_ggplot(e1, e2, e2name)
  else if (is.ggproto(e1)) {
    stop("Cannot add ggproto objects together.",
         " Did you forget to add this object to a ggplot object?",
         call. = FALSE)
  }
}

(https://github.com/tidyverse/ggplot2/blob/7d0549a03e5ea08c27c768e88d5717f18cb4a5ce/R/plot-construction.r#L40-L52)

add_ggplot <- function(p, object, objectname) {
  if (is.null(object)) return(p)

  p <- plot_clone(p)
  p <- ggplot_add(object, p, objectname)
  set_last_plot(p)
  p
}

(https://github.com/tidyverse/ggplot2/blob/7d0549a03e5ea08c27c768e88d5717f18cb4a5ce/R/plot-construction.r#L59-L66)

ggplot_add()object、つまり+の右側にくるオブジェクトのクラスによって適切なメソッドを選びます。

なので、geom_sugoi()は独自クラスsugoiのオブジェクトを返すようにして、sugoi用のggplot_add()のメソッドを以下のように実装すると、

geom_sugoi <- function(expr) {
  structure(list(expr = rlang::enquo(expr)), class = "sugoi")
}

ggplot_add.sugoi <- function(object, plot, object_name) {
  new_data <- dplyr::filter(plot$data, !! object$expr)
  new_layer <- geom_point(data = new_data,
                          mapping = plot$mapping,
                          # 強調のため色とサイズを変える
                          colour = alpha("red", 0.5),
                          size = 5)
  plot$layers <- append(plot$layers, new_layer)
  plot
}

こんな風なグラフが描けるようになります。強調されている部分がわかりやすいように素のgeom_point()をひとつ重ねています。

ggplot(moto_no_data, aes(x = oops, y = alas)) +
  geom_point() +
  geom_sugoi(expr = sugosa > 5)

f:id:yutannihilation:20171107102557p:plain

ということで、+.ggを上書きするなんていう危険なまねをしなくてもよくなりました。

注意点

ggplot_add()は次期バージョンのggplot2に入っているということで、上に書いた例を試すにはdevtools::install_github("tidyverse/ggplot2")で開発版のggplot2をインストールする必要があります。たぶんそう遠くないうちにリリースがあるんじゃないかなあと思ってるんですが、NEWSを見てるとけっこう変更内容が多いので混乱しそうです。このへんが安定して使えるようになるのはもうちょっと先かもしれません。

*1:できませんといいつつ黒魔術を使えばたぶんできる気はするけど...