パッケージでの ggplot2 の使用

この vignette は、自分のパッケージコードの中で ggplot2 を使用するパッケージ開発者を対象としています。 この記事を書いている時点で、CRAN上の2,000以上のパッケージが含まれており、他の場所でも多くのパッケージが含まれています。 パッケージ内で ggplot2 を使ってプログラミングすることは、特にパッケージをCRANに提出したい場合、いくつかの制約を加えます。 特に、Rパッケージ内でのプログラミングは、ggplot2 の関数を参照する方法と、aes()vars() 内で ggplot2 の非標準的な評価を使用する方法を変えます。

ggplot2 の関数を参照する

他のパッケージからの関数と同様に、DESCRIPTIONImports に ggplot2 を列挙し、:: を使用してその関数を参照する必要があります (例: ggplot2::function_name)。:

mpg_drv_summary <- function() {
  ggplot2::ggplot(ggplot2::mpg) + 
    ggplot2::geom_bar(ggplot2::aes(x = .data$drv)) + 
    ggplot2::coord_flip()
}

ggplot2 の関数を頻繁に使用する場合,ggplot2 から1つ以上の関数を自分のNAMESPACEにインポートしたいと思うかもしれません。 roxygen2 を使用している場合、任意の roxygen コメントブロックに #' @importFrom ggplot2 <1つ以上のオブジェクト名> を含めることができます(これは mpg のようなデータセットには使えません)。

#' @importFrom ggplot2 ggplot aes geom_bar coord_flip
mpg_drv_summary <- function() {
  ggplot(ggplot2::mpg) + 
    geom_bar(aes(x = drv)) + 
    coord_flip()
}

あなたのパッケージで多くの ggplot2 関数を使用する場合でも、Depends で ggplot2 を使用したり、パッケージ全体をあなたの NAMESPACE にインポートすることは賢明ではありません (例えば、#' @import ggplot2 を使用します)。 Depends で ggplot2 を使用すると、あなたのパッケージがアタッチされたときに ggplot2 がアタッチされますが、これにはあなたのパッケージがテストされたときも含まれます。 これにより、他の人がパッケージをアタッチせずに(すなわち、:: を使用して)あなたのパッケージ内の関数を使用できることを保証することが難しくなります。 同様に、あなたの名前空間に ggplot2 の 450個のエクスポートされたオブジェクトのすべてをインポートすることは、あなたのパッケージの責任と ggplot2 の責任を分離することを困難にし、加えて、あなたのコードの読者が関数がどこから来ているのかを理解することを困難にします。

パッケージ関数での aes()vars() の使用

ggplot2 を使ってグラフィックを作成する場合、おそらく一度は aes() を使う必要があるでしょう。また、グラフィックが facet を使用している場合、プロット/レイヤーデータの列を参照するために、vars() を使用しているかもしれません。 これらの関数はいずれも非標準的な評価を行うため、パッケージ内の関数で使おうとすると、CMD check ノートが表示されます。:

mpg_drv_summary <- function() {
  ggplot(ggplot2::mpg) + 
    geom_bar(aes(x = drv)) + 
    coord_flip()
}
N  checking R code for possible problems (2.7s)
   mpg_drv_summary: no visible binding for global variable ‘drv’
   Undefined global functions or variables:
     drv

この問題に遭遇するのは3つの状況です。

もし、(上の例のように)事前にマッピングがわかっている場合は、rlang.data という代名詞を使って、 レイヤーデータの中の drv を参照しているのであって、drv という名前の他の変数(他の場所に存在するかもしれないし、存在しないかもしれない)を参照しているのではないことを明示してください。 CMD check で .data についての同様の注意を避けるために、あらゆる roxygen のコードブロックで #' @importFrom rlang .data を使用してください (通常、これは usethis::use_package_doc() で生成されたパッケージドキュメントの中にあるはずです)。

mpg_drv_summary <- function() {
  ggplot(ggplot2::mpg) + 
    geom_bar(aes(x = .data$drv)) + 
    coord_flip()
}

列名が文字ベクトルの場合(例:col = "drv")、.data[[col]] を使用します。:

col_summary <- function(df, col) {
  ggplot(df) + 
    geom_bar(aes(x = .data[[col]])) + 
    coord_flip()
}

col_summary(mpg, "drv")

列名や式がユーザーによって提供される場合は、{{ col }}を使って aes()vars() に渡すこともできます。 この tidy eval 演算子は、ユーザーによって供給された式を捕捉し、それを aes()vars() などの別の tidy eval 対応関数に転送します。

col_summary <- function(df, col) {
  ggplot(df) + 
    geom_bar(aes(x = {{ col }})) + 
    coord_flip()
}

col_summary(mpg, drv)

要約すると

他にも様々な方法がありますが、ここで使用している構文は将来的にも動作することが保証されている唯一の構文です。 特に、aes_()aes_string() は非推奨であり、将来のバージョンで削除される可能性があるため、使用しないでください。 最後に、データフレームとマッピングを作成して ggplot() やそのレイヤーに渡すステップを省略しないでください。 最後に、ggplot()やそのレイヤーに渡すデータフレームとマッピングを作成するステップを省略しないでください! 他の方法もありますが、文書化されていない動作に依存していたり、予期せぬ方法で失敗することがあります。

一般的なタスクのベストプラクティス

ggplot2 を使ったオブジェクトの可視化

ggplot2 は、パッケージの中で、オブジェクトを可視化するために(例えば、plot() スタイルの関数で)よく使われます。 例えば、あるパッケージで、様々な離散的な値の確率を表すS3クラスを定義する場合があります。:

mpg_drv_dist <- structure(
  c(
    "4" = 103 / 234,
    "f" = 106 / 234,
    "r" = 25 / 234
  ),
  class = "discrete_distr"
)

Rの多くの S3 クラスは plot() メソッドを持っていますが、1つの plot() メソッドでユーザーが求める視覚化を実現できると期待するのは非現実的です。 しかし、ユーザがオブジェクトの本質を理解するために呼び出すことができる視覚的な要約として、plot()メソッドを提供することは有用です。 すべてのユーザを満足させるためには、オブジェクトをデータフレームに変換する関数を書くことをお勧めします (オブジェクトがより複雑な場合は,データフレームの list() に変換します)。 このアプローチの良い例が ggdendro で、これは ggplot2 を使ってデンドログラムを作成するだけでなく、ユーザが自分で作成するのに必要なデータを計算します。 上記の例では、関数は次のようになります。:

discrete_distr_data <- function(x) {
  tibble::tibble(
    value = names(x),
    probability = as.numeric(x)
  )
}

discrete_distr_data(mpg_drv_dist)
#> # A tibble: 3 x 2
#>   value probability
#>   <chr>       <dbl>
#> 1 4           0.440
#> 2 f           0.453
#> 3 r           0.107

一般的に、plot() を使う人は、グラフィックが表示されるという副次的な効果を期待してこのコマンドを呼び出します。 これは、明示的に print() されない限り表示されない ggplot() の動作とは異なります。 このため、ggplot2 は独自の generic な autoplot() を定義しており、これを呼び出すと ggplot() を (副作用なしに) 返します。

#' @importFrom ggplot2 autoplot
autoplot.discrete_distr <- function(object, ...) {
  plot_data <- discrete_distr_data(object)
  ggplot(plot_data, aes(.data$value, .data$probability)) +
    geom_col() +
    coord_flip() +
    labs(x = "Value", y = "Probability")
}

autoplot() メソッドが定義されると、plot() メソッドは autoplot() の結果を print() することで構成されるようになります。

#' @importFrom graphics plot
plot.discrete_distr <- function(x, ...) {
  print(autoplot(x, ...))
}

S3 クラスを所有していないのに、plot()autoplot() のような S3 generic を実装することは、S3 をコントロールしているパッケージ開発者が自分でそのメソッドを実装することを困難にするため、悪い習慣だと考えられています。 これは、S3 を管理しているパッケージ開発者が自分でそのメソッドを実装することを困難にするからです。

新しいテーマの作成

新しいテーマを作成する際には、既存のテーマ(例:theme_grey())から始めて、変更すべき要素を %+replace% するのが常套手段です。 一見すべての要素が置き換えられているように見えても、これは正しい戦略です。 そうしないと、新しい要素を追加してテーマを改善することが難しくなるからです。 ggthemes パッケージにはテーマの優れた例がたくさんあります。

#' @importFrom ggplot2 %+replace%
theme_custom <- function(...) {
  theme_grey(...) %+replace% 
    theme(
      panel.border = element_rect(size = 1, fill = NA),
      panel.background = element_blank(),
      panel.grid = element_line(colour = "grey80")
    )
}

mpg_drv_summary() + theme_custom()

パッケージがロードされた後にテーマが計算されることが重要です。 そうしないと、テーマオブジェクトはビルドされたパッケージのコンパイルされたバイトコードに格納され、それはインストールされた ggplot2 のバージョンと一致するかもしれません。 もしあなたのパッケージがその可視化のためのデフォルトテーマを持っているなら、それをロードする正しい方法は、デフォルトテーマを返す関数を持つことです。

default_theme <- function() {
  theme_custom()
}

mpg_drv_summary2 <- function() {
  mpg_drv_summary() + default_theme()
}

ggplot2 の出力のテスト

ggplot2 の出力をテストするには、ビジュアルテストケースを管理するツールである vdiffr パッケージを使用することをお勧めします(これは ggplot2 をテストする方法の一つです)。 ggplot2 やあなたのコードの変更が ggplot の視覚的な出力の変更をもたらすならば、テストはローカルやTravis上でそれらを実行するときに失敗するでしょう。 vdiffr を使用するには、testthat を使用していることを確認し (始めるには usethis::use_testthat() を使用することができます)、DESCRIPTIONSuggests に vdiffr を追加します。 そして、vdiffr::expect_doppleganger(<name of plot>, <ggplot object>) を使って、<ggplot object> に視覚的な変化があった場合に失敗するテストを作ります。

test_that("ggplot()の出力は安定している", {)
  vdiffr::expect_doppelganger("A blank plot", ggplot())
})

ggplot2 の Suggests への参加

自分のパッケージで ggplot2 を使用する場合、ほとんどの場合、Importsにリストアップしたいと思います。 代わりに Suggests に ggplot2 をリストアップする場合は、 #' @importFrom ggplot2 ... ができなくなります (つまり、ggplot2 オブジェクトを :: を使って参照する必要があります)。 %+replace% のような ggplot2 からの infix 演算子を使用していて、Suggests で ggplot2 を維持したい場合には、演算子が使用される前に関数内で演算子を割り当てることができます。

theme_custom <- function(...) {
  `%+replace%` <- ggplot2::`%+replace%`
  
  ggplot2::theme_grey(...) %+replace% 
    ggplot2::theme(panel.background = ggplot2::element_blank())
}

一般的に、もしあなたが autoplot() のような ggplot2 genericのためのメソッドを追加するならば、 ggplot2 は Imports にあるべきです。 何らかの理由で ggplot2 を Suggests に入れておきたい場合は、vctrs::s3_register() を使って ggplot2 がインストールされている場合にのみ、ジェネリックを登録することができます。 これを行う場合は、vctrs の依存関係を追加しないように、vctrs::s3_register() のソースを自分のパッケージにコピー&ペーストする必要があります。

.onLoad <- function(...) {
  if (requireNamespace("ggplot2", quietly = TRUE)) {
    vctrs::s3_register("ggplot2::autoplot", "discrete_distr")
  }
}