読者です 読者をやめる 読者になる 読者になる

ggplot2のコードをなんとなく追ってみつつ自作パッケージの方針を練る

R ggplot2 htmlwidgets

引き続きmetrics-graphics.jshtmlwidgetsでRに取り込もうとしているわけですが、さっそくドキュメントを見ると

List of Options · mozilla/metrics-graphics Wiki · GitHub

オプション多すぎやろ!

というわけで、これはぜんぶのパラメータを一つの関数に渡すよりは、いくつかの単位に区切るとかした方が使い勝手が良くなる感じがします。

Javascriptの可視化をRに持ち込もうと志同じくする(とか言うと畏れ多すぎますけど...)dygraphs(http://rstudio.github.io/dygraphs/)がどうやってるのかチラ見してみると、設定する用の関数はいい感じに分けられています。で、この例でmagrittrの%>%が使われているのを見て、

dygraph(lungDeaths) %>%
  dySeries("mdeaths", label = "Male") %>%
  dySeries("fdeaths", label = "Female") %>%
  dyOptions(stackedGraph = TRUE) %>%
  dyRangeSelector(height = 20)

あれ? でもこれ%>%使わなくてもggplot2で+ってやってるみたいにつなげられることない?と思ったのがきっかけでちょっとなんとなくコード追ってみました。

+

ggplot2はggクラス用の+を定義しています。この辺の挙動については、?`+.gg`と打つとヘルプが出ます。

(関数の後ろに.でクラス名をつなげる、というのはS3クラス用のメソッドのつくりかたです。このへんは長くなるので説明は省きますが、HadleyさんのAdvanced Rとか、このブログの昔の記事とかが参考になるかもしれません)

"+.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)
}

(https://github.com/hadley/ggplot2/blob/4bb9270ef4d5d5062353438fd99d17b6f6de98a2/R/plot-construction.r#L63-L70)

themeの場合は置いておいて、これはつまりggクラスどうしを+したときは、実際にはadd_ggplot()が呼ばれているということです。

add_ggplot()は同じソースファイルのちょっと下に書いてあります。

add_ggplot()

長いので途中まで。こんな感じです。

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

  p <- plot_clone(p)
  if (is.data.frame(object)) {
    p$data <- object
  } else if (is.theme(object)) {
    p$theme <- update_theme(p$theme, object)
  } else if (inherits(object, "scale")) {
    p$scales$add(object)
  } else if(inherits(object, "labels")) {
    p <- update_labels(p, object)
  } else if(inherits(object, "guides")) {
    p <- update_guides(p, object)
...snip...

(https://github.com/hadley/ggplot2/blob/4bb9270ef4d5d5062353438fd99d17b6f6de98a2/R/plot-construction.r#L63-L70)

よく分かりませんが。とりあえず、やっていることは、

  1. 元のggオブジェクト(p)をクローンする
  2. 足し合わされたのがscalelabelsかとかで場合分けがあって、それぞれに応じて用意されているupdate_XXX()みたいな関数が呼ばれる

みたいです。

試しにひとつみてみます。

update_labels()

update_labels <- function(p, labels) {
  p <- plot_clone(p)
  p$labels <- defaults(labels, p$labels)
  p
}

(https://github.com/hadley/ggplot2/blob/4bb9270ef4d5d5062353438fd99d17b6f6de98a2/R/labels.r#L12-L16)

defaults()というのは、plyrの関数らしいです。(plyr/defaults.r at master · hadley/plyr · GitHub)1つ目の引数に値のリストを、2つ目の引数にデフォルト値のリストを取って、1つ目に含まれていない要素はデフォルト値で埋めて返してくれる便利関数みたいです。

結局やってることは

  1. pをクローン
  2. 新しいlabelsを元のp$labelsとマージ
  3. 新しい値をセットしたpを返す

だけです。

ということで、+でつないで色んな値を更新していく、ということは分かりました。でもこれだけではただのクラスです。

qplot(1:10, runif(10), geom="point")

とか打っただけでグラフが表示されるのはどういう仕組みかというと、ggplot用のprint()が用意されているからです。

print.ggplot()

print.ggplot <- function(x, newpage = is.null(vp), vp = NULL, ...) {
  set_last_plot(x)
  if (newpage) grid.newpage()

  data <- ggplot_build(x)

  gtable <- ggplot_gtable(data)
  if (is.null(vp)) {
    grid.draw(gtable)
  } else {
    if (is.character(vp)) seekViewport(vp) else pushViewport(vp)
    grid.draw(gtable)
    upViewport()
  }

  invisible(data)
}

(https://github.com/hadley/ggplot2/blob/master/R/plot-render.r#L179-L195)

たとえばこれを上書きしてやると、グラフは表示されず、オブジェクトが持つ要素が味気なくずらっと表示されるだけです。

> print.ggplot <- print.default
> qplot(1:10,runif(10), geom="point")
$data
data frame with 0 columns and 0 rows

$layers
$layers[[1]]
geom_point:  
stat_identity:  
position_identity: (width = NULL, height = NULL)
...

まとめ

ということで、S3のクラスをつくって+printを上書きしてやればたぶんggplot2っぽい挙動が真似できそうな気がしてきました。