How to customize the printed output of a "tbl"
subclass?
This vignette shows the various customization options. Customizing the
formatting of a vector class in a tibble is described in
vignette("pillar", package = "vctrs")
. An overview over the
control and data flow is given in vignette("printing")
.
This vignette assumes that the reader is familiar with S3 classes, methods, and inheritance. The “S3” chapter of Hadley Wickham’s “Advanced R” is a good start.
To make use of pillar’s printing capabilities, create a class that
inherits from "tbl"
, like tibble (classes
"tbl_df"
and "tbl"
), dbplyr lazy tables
("tbl_lazy"
and "tbl"
) and sf spatial data
frames ("sf"
, "tbl_df"
and
"tbl"
). Because we are presenting various customization
options, we create a constructor for an example data frame with
arbitrary subclass.
example_tbl <- function(class) {
vctrs::new_data_frame(
list(
a = letters[1:3],
b = data.frame(c = 1:3, d = 4:6 + 0.5)
),
class = c(class, "tbl")
)
}
The "default"
class doesn’t have any customizations yet,
and prints like a regular tibble.
example_tbl("default")
#> $a
#> [1] "a" "b" "c"
#>
#> $b
#> c d
#> 1 1 4.5
#> 2 2 5.5
#> 3 3 6.5
#>
#> attr(,"class")
#> [1] "default" "tbl" "data.frame"
The easiest customization consists of tweaking the header. Implement
a tbl_sum()
method to extend or replace the information
shown in the header, keeping the original formatting.
tbl_sum.default_header_extend <- function(x, ...) {
default_header <- NextMethod()
c(default_header, "New" = "A new header")
}
example_tbl("default_header_extend")
#> # A data frame: 3 × 2
#> # New: A new header
#> a b$c $d
#> <chr> <int> <dbl>
#> 1 a 1 4.5
#> 2 b 2 5.5
#> 3 c 3 6.5
tbl_sum.default_header_replace <- function(x, ...) {
c("Override" = "Replace all headers")
}
example_tbl("default_header_replace")
#> # Override: Replace all headers
#> a b$c $d
#> <chr> <int> <dbl>
#> 1 a 1 4.5
#> 2 b 2 5.5
#> 3 c 3 6.5
To style the header in a different way, implement a
tbl_format_header()
method. The implementation is
responsible for the entire formatting and styling, including the leading
hash.
By implementing the generic ctl_new_rowid_pillar()
,
printing of the row ID column can be customized. In order to print Roman
instead of Arabic numerals, one could use utils::as.roman()
to generate the corresponding sequence and build up a row ID pillar
using new_pillar()
and associated methods as has been
introduced previously.
ctl_new_rowid_pillar.pillar_roman <- function(controller, x, width, ...) {
out <- NextMethod()
rowid <- utils::as.roman(seq_len(nrow(x)))
width <- max(nchar(as.character(rowid)))
new_pillar(
list(
title = out$title,
type = out$type,
data = pillar_component(
new_pillar_shaft(list(row_ids = rowid),
width = width,
class = "pillar_rif_shaft"
)
)
),
width = width
)
}
example_tbl("pillar_roman")
#> # A data frame: 3 × 2
#> a b$c $d
#> <chr> <int> <dbl>
#> I a 1 4.5
#> II b 2 5.5
#> III c 3 6.5
Pillars consist of components, see ?new_pillar_component
for details. Extend or override the ctl_new_pillar()
method
to alter the appearance. The example below adds table rules of constant
width to the output.
ctl_new_pillar.pillar_rule <- function(controller, x, width, ..., title = NULL) {
out <- NextMethod()
new_pillar(list(
top_rule = new_pillar_component(list("========"), width = 8),
title = out$title,
type = out$type,
mid_rule = new_pillar_component(list("--------"), width = 8),
data = out$data,
bottom_rule = new_pillar_component(list("========"), width = 8)
))
}
example_tbl("pillar_rule")
#> # A data frame: 3 × 2
#> ======== ======== ========
#> a b$c $d
#> <chr> <int> <dbl>
#> -------- -------- --------
#> 1 a 1 4.5
#> 2 b 2 5.5
#> 3 c 3 6.5
#> ======== ======== ========
To make the width adaptive, we implement a "rule"
class
with a format()
method that formats rules to prespecified
widths.
rule <- function(char = "-") {
stopifnot(nchar(char) == 1)
structure(char, class = "rule")
}
format.rule <- function(x, width, ...) {
paste(rep(x, width), collapse = "")
}
ctl_new_pillar.pillar_rule_adaptive <- function(controller, x, width, ..., title = NULL) {
out <- NextMethod()
if (is.null(out)) {
return(NULL)
}
new_pillar(list(
top_rule = new_pillar_component(list(rule("=")), width = 1),
title = out$title,
type = out$type,
mid_rule = new_pillar_component(list(rule("-")), width = 1),
data = out$data,
bottom_rule = new_pillar_component(list(rule("=")), width = 1)
))
}
example_tbl("pillar_rule_adaptive")
#> # A data frame: 3 × 2
#> = = =
#> a b$c $d
#> <chr> <int> <dbl>
#> - - -
#> 1 a 1 4.5
#> 2 b 2 5.5
#> 3 c 3 6.5
#> = = =
Compound pillars are created by ctl_new_pillar_list()
for columns that contain a data frame, a matrix or an array. The default
implementation also calls ctl_new_pillar()
shown above. The
(somewhat artificial) example hides all data frame columns in a column
with the type "<hidden>"
.
ctl_new_pillar_list.hide_df <- function(controller, x, width, ..., title = NULL) {
if (!is.data.frame(x)) {
return(NextMethod())
}
if (width < 8) {
return(NULL)
}
list(new_pillar(
list(
title = pillar_component(new_pillar_title(title)),
type = new_pillar_component(list("<hidden>"), width = 8),
data = new_pillar_component(list(""), width = 1)
),
width = 8
))
}
example_tbl("hide_df")
#> # A data frame: 3 × 2
#> <hidden>
#>
#> 1 <hidden>
#> 2
#> 3 <hidden>
Last but not least, it is also possible to completely alter the
display of the body by overriding tbl_format_body()
. The
example below uses plain data frame output for a tibble.
tbl_format_body.oldskool <- function(x, setup, ...) {
capture.output(print.data.frame(setup$df))
}
print(example_tbl("oldskool"), n = 2)
#> # A data frame: 3 × 2
#> a b.c b.d
#> 1 a 1 4.5
#> 2 b 2 5.5
#> # ℹ 1 more row
Note that default printed output is computed in
tbl_format_setup()
, this takes a considerable amount of
time. If you really need to change the output for the entire body,
consider providing your own tbl_format_setup()
method.