ベクトルを tibbles できれいに表示

Kirill Müller, Hadley Wickham

format()メソッドを用意することで、tibble にベクトルがどのように印刷されるかを基本的に制御することができます。 もっとコントロールしたいのであれば、印刷の仕組みを理解する必要があります。 tibble 内のカラムの表示は、2つのS3ジェネリックで制御されます。

技術的には pillar は、(ornamentで飾られた)shaft と、上に capital、下に base で構成されています。 複数の柱は、複数の階層に積み重ねることができるコロネードを形成します。 これが、私たちのAPIの名前の背景にある動機です。

この短い vignette では、latlon ベクトルを使ったカラムスタイリングの基本を紹介します。 この vignette では、コードがパッケージに入っていることを想定しており、ドキュメント作成に必要な roxygen2 のコマンドや、NAMESPACEファイルを確認することができます。 この vignette では、pillar と vctrs を attach します。

library(vctrs)
library(pillar)

これをパッケージで行う必要はありません。 代わりに、DESCRIPTIONImports: セクションに、パッケージを import する必要があります。 以下のヘルパーがこれを行ってくれます。

usethis::use_package("vctrs")
usethis::use_package("pillar")

前提条件

基本的なアイデアを説明するために、地理的な座標をレコードにエンコードする "latlon" クラスを作成します。 このコードは earth というパッケージに入っていることにしましょう。 簡単にするために,値は度と分だけで表示されます。 vctrs_rcrd() を使うことで,このクラスをデータフレームと完全に互換性のあるものにするためのインフラがすでに無料で手に入ります。 レコードデータタイプの詳細については、vignette("s3-vector", package = "vctrs") を参照してください。

#' @export
latlon <- function(lat, lon) {
  new_rcrd(list(lat = lat, lon = lon), class = "earth_latlon")
}

#' @export
format.earth_latlon <- function(x, ..., formatter = deg_min) {
  x_valid <- which(!is.na(x))

  lat <- field(x, "lat")[x_valid]
  lon <- field(x, "lon")[x_valid]

  ret <- rep(NA_character_, vec_size(x))
  ret[x_valid] <- paste0(formatter(lat, "lat"), " ", formatter(lon, "lon"))
  # It's important to keep NA in the vector!
  ret
}

deg_min <- function(x, direction) {
  pm <- if (direction == "lat") c("N", "S") else c("E", "W")

  sign <- sign(x)
  x <- abs(x)
  deg <- trunc(x)
  x <- x - deg
  min <- round(x * 60)

  # Ensure the columns are always the same width so they line up nicely
  ret <- sprintf("%d°%.2d'%s", deg, min, ifelse(sign >= 0, pm[[1]], pm[[2]]))
  format(ret, justify = "right")
}

latlon(c(32.71, 2.95), c(-117.17, 1.67))
#> <earth_latlon[2]>
#> [1] 32°43'N 117°10'W  2°57'N   1°40'E

tibble での使用

このクラスの列は,vctrs のインフラを利用してクラスを作成し,format() メソッドを提供しているので,すぐに tibble で使用することができます。:

library(tibble)
#> 
#>  次のパッケージを付け加えます: 'tibble'
#>  以下のオブジェクトは 'package:vctrs' からマスクされています:
#> 
#>     data_frame

loc <- latlon(
  c(28.3411783, 32.7102978, 30.2622356, 37.7859102, 28.5, NA),
  c(-81.5480348, -117.1704058, -97.7403327, -122.4131357, -81.4, NA)
)

data <- tibble(venue = "rstudio::conf", year = 2017:2022, loc = loc)

data
#> # A tibble: 6 × 3
#>   venue          year              loc
#>   <chr>         <int>       <erth_ltl>
#> 1 rstudio::conf  2017 28°20'N  81°33'W
#> 2 rstudio::conf  2018 32°43'N 117°10'W
#> 3 rstudio::conf  2019 30°16'N  97°44'W
#> 4 rstudio::conf  2020 37°47'N 122°25'W
#> 5 rstudio::conf  2021 28°30'N  81°24'W
#> 6 rstudio::conf  2022               NA

この出力はOKですが、次のように改善することができます。

  1. <erth_ltl> よりももっと説明的なタイプの略語を使う。

  2. 値の最も重要な部分を強調するために色のダッシュを使用する。

  3. 水平方向のスペースが限られている場合に、より狭い表示を提供する。

次のセクションでは、レンダリングを強化する方法を紹介します。

データ型を修正する

<erth_ltl> の代わりに <latlon> を使いたいと思います。 これを実現するには、vec_ptype_abbr() メソッドを実装して、列のヘッダに使える文字列を返すようにします。 自分のクラスでは、6文字以内のわかりやすい略語を使うようにしましょう。

#' @export
vec_ptype_abbr.earth_latlon <- function(x) {
  "latlon"
}

data
#> # A tibble: 6 × 3
#>   venue          year              loc
#>   <chr>         <int>         <latlon>
#> 1 rstudio::conf  2017 28°20'N  81°33'W
#> 2 rstudio::conf  2018 32°43'N 117°10'W
#> 3 rstudio::conf  2019 30°16'N  97°44'W
#> 4 rstudio::conf  2020 37°47'N 122°25'W
#> 5 rstudio::conf  2021 28°30'N  81°24'W
#> 6 rstudio::conf  2022               NA

レンダリングのカスタマイズ

レンダリングにはデフォルトで format() メソッドが使われます。 カスタムフォーマットのためには、pillar_shaft() メソッドを実装する必要があります。 この関数は常に new_pillar_shaft_simple() などで作成されたピラーシャフトオブジェクトを返す必要があります。 new_pillar_shaft_simple() ではANSIエスケープコードによる色付けが可能で、pillar には style_subtle() のようなスタイルが組み込まれています。 ここでは、データをよりわかりやすくするために、度と分のセパレータに微妙なスタイルを使うことができます。

まず、style_subtle() を利用する度数フォーマッタを定義します。

deg_min_color <- function(x, direction) {
  pm <- if (direction == "lat") c("N", "S") else c("E", "W")

  sign <- sign(x)
  x <- abs(x)
  deg <- trunc(x)
  x <- x - deg
  rad <- round(x * 60)
  ret <- sprintf(
    "%d%s%.2d%s%s",
    deg,
    pillar::style_subtle("°"),
    rad,
    pillar::style_subtle("'"),
    pm[ifelse(sign >= 0, 1, 2)]
  )
  format(ret, justify = "right")
}

そして、それを format() メソッドに渡します。

#' @importFrom pillar pillar_shaft
#' @export
pillar_shaft.earth_latlon <- function(x, ...) {
  out <- format(x, formatter = deg_min_color)
  pillar::new_pillar_shaft_simple(out, align = "right")
}

現在、ANSI エスケープは vignette ではレンダリングされないので、この結果は何も変わっていませんが、自分でコードを走らせてみると、表示が改善されているのがわかります。

data
#> # A tibble: 6 × 3
#>   venue          year              loc
#>   <chr>         <int>         <latlon>
#> 1 rstudio::conf  2017 28°20'N  81°33'W
#> 2 rstudio::conf  2018 32°43'N 117°10'W
#> 3 rstudio::conf  2019 30°16'N  97°44'W
#> 4 rstudio::conf  2020 37°47'N 122°25'W
#> 5 rstudio::conf  2021 28°30'N  81°24'W
#> 6 rstudio::conf  2022               NA

pillar の機能に加えて、cli パッケージは、テキストにスタイルを付けるための様々なツールを提供します。

切り捨て

tibble は、すべてを表示するのに十分な水平方向のスペースがない場合、自動的に列をコンパクトにすることができます。

print(data, width = 30)
#> # A tibble: 6 × 3
#>   venue  year              loc
#>   <chr> <int>         <latlon>
#> 1 rstu…  2017 28°20'N  81°33'W
#> 2 rstu…  2018 32°43'N 117°10'W
#> 3 rstu…  2019 30°16'N  97°44'W
#> 4 rstu…  2020 37°47'N 122°25'W
#> 5 rstu…  2021 28°30'N  81°24'W
#> 6 rstu…  2022               NA

現在、シャフトを構成する際に最小の幅を指定していないため、latlon クラスがコンパクトになることはありません。 これを修正して、データを再表示しましょう。

#' @importFrom pillar pillar_shaft
#' @export
pillar_shaft.earth_latlon <- function(x, ...) {
  out <- format(x)
  pillar::new_pillar_shaft_simple(out, align = "right", min_width = 10)
}

print(data, width = 30)
#> # A tibble: 6 × 3
#>   venue        year       loc
#>   <chr>       <int>  <latlon>
#> 1 rstudio::c…  2017 28°20'N …
#> 2 rstudio::c…  2018 32°43'N …
#> 3 rstudio::c…  2019 30°16'N …
#> 4 rstudio::c…  2020 37°47'N …
#> 5 rstudio::c…  2021 28°30'N …
#> 6 rstudio::c…  2022        NA

適応レンダリング

文字データの場合は切り捨てが有効ですが、緯度・経度データの場合は、完全な度数を表示して分を削除する方が良いでしょう。 まずはこれを行う関数を書いてみましょう。

deg <- function(x, direction) {
  pm <- if (direction == "lat") c("N", "S") else c("E", "W")

  sign <- sign(x)
  x <- abs(x)
  deg <- round(x)

  ret <- sprintf("%d°%s", deg, pm[ifelse(sign >= 0, 1, 2)])
  format(ret, justify = "right")
}

その後、より洗練された pillar_shaft() メソッドの実装の一部として使用します。

#' @importFrom pillar pillar_shaft
#' @export
pillar_shaft.earth_latlon <- function(x, ...) {
  deg <- format(x, formatter = deg)
  deg_min <- format(x)

  pillar::new_pillar_shaft(
    list(deg = deg, deg_min = deg_min),
    width = pillar::get_max_extent(deg_min),
    min_width = pillar::get_max_extent(deg),
    class = "pillar_shaft_latlon"
  )
}

ここで、pillar_shaft() メソッドは、new_pillar_shaft() で作成したクラス "pillar_shaft_latlon" のオブジェクトを返します。 このオブジェクトには、値をレンダリングするために必要な情報と、幅の最小値と最大値が含まれています。

簡単にするために,どちらのフォーマットもプリレンダリングされ,そこから最小幅と最大幅が計算されます。 (get_max_extent() は、文字ベクトルの値が占めるディスプレイの最大幅を計算するヘルパーです)。

あとは、新しい "pillar_shaft_latlon" クラスに format() メソッドを実装するだけです。 このメソッドは、widthの引数で呼び出され、どのフォーマットを選択するかを決定します。 選んだフォーマットは new_ornament() 関数に渡されます。

#' @export
format.pillar_shaft_latlon <- function(x, width, ...) {
  if (get_max_extent(x$deg_min) <= width) {
    ornament <- x$deg_min
  } else {
    ornament <- x$deg
  }

  pillar::new_ornament(ornament, align = "right")
}

data
#> # A tibble: 6 × 3
#>   venue          year              loc
#>   <chr>         <int>         <latlon>
#> 1 rstudio::conf  2017 28°20'N  81°33'W
#> 2 rstudio::conf  2018 32°43'N 117°10'W
#> 3 rstudio::conf  2019 30°16'N  97°44'W
#> 4 rstudio::conf  2020 37°47'N 122°25'W
#> 5 rstudio::conf  2021 28°30'N  81°24'W
#> 6 rstudio::conf  2022               NA
print(data, width = 30)
#> # A tibble: 6 × 3
#>   venue        year        loc
#>   <chr>       <int>   <latlon>
#> 1 rstudio::c…  2017 28°N  82°W
#> 2 rstudio::c…  2018 33°N 117°W
#> 3 rstudio::c…  2019 30°N  98°W
#> 4 rstudio::c…  2020 38°N 122°W
#> 5 rstudio::c…  2021 28°N  81°W
#> 6 rstudio::c…  2022         NA

テスト

コードの出力をテストしたい場合、テキストファイルに記録された既知の状態と比較することができます。 testthat::expect_snapshot() 関数は、出力を生成する関数を簡単にテストする方法を提供します。 この関数は、UnicodeやANSIエスケープ、出力幅などの詳細を考慮します。 さらに、CRAN上でテストが失敗することもありません。これは重要なことです。 なぜなら、あなたの出力はあなたがコントロールできない詳細に依存しているかもしれないからです。 これらはいずれ修正されるべきですが、あなたのパッケージがCRANから削除されるようなことがあってはなりません。

スナップショットテストを作成するには、テストファイルの1つにこの testthat 期待値を使用します。:

expect_snapshot(pillar_shaft(data$loc))

詳しくはhttps://testthat.r-lib.org/articles/snapshotting.htmlをご覧ください。