getS3method('knit_print', 'default')
function (x, ..., inline = FALSE)
{
if (inline)
x
else normal_print(x)
}
<bytecode: 0x0000000012a047f8>
<environment: namespace:knitr>
In a previous post, knitr_tabset
was introduced as a mean of programmatic tabsets. Can we improve this function using knit_print
?
Josh Cowley
November 10, 2022
Knitr is printing via a generic knit_print
meaning tabsets can be made automatic by including a specific method for lists.
If the jcutils
package is installed (from GitHub) then,
will convert list (including nested and ragged list) output to tabsets.
In a previous blog post, I introduced a function that would take a list and print the contents as tabsets.
A few issues with this method include
the need for the results: asis
chunk option,
the workaround (and knowledge) needed for gt
, that is,
It was actually a gt
table that was causing a report to fail that inspired me to look for a better solution.
On stack overflow and related Quarto issue comment, Christophe Dervieux recommends using knit_child
to create dynamic content and also mentions the knit_print
function.
knit_print
?The related vignette for this function explains the process in great detail so I won’t say too much, but fundamentally
the function is a generic meaning it calls knit_print.foo
for an object with class foo
,
it is the default argument of the render
chunk option called on all output.
Most of the time it is going to default to simply printing the object
function (x, ..., inline = FALSE)
{
if (inline)
x
else normal_print(x)
}
<bytecode: 0x0000000012a047f8>
<environment: namespace:knitr>
normal_print
is just used to use the S4 method show
in place of print
for S4 objects.
We can intercept or overwrite this functionality as required. Suppose we define the following custom method, with specific argument names (see the vignette for more information on these).
To apply this to a single chunk we use render: !expr knit_print_strsplit
and to apply this globally we define (and register) an S3 method for character objects.
knit_print.character <- knit_print_strsplit
registerS3method("knit_print", "character", knit_print.character)
When applied, any visible character object has an altered output.
[1] "H"
[1] "e"
[1] "l"
[1] "l"
[1] "o"
[1] " "
[1] "W"
[1] "o"
[1] "r"
[1] "l"
[1] "d"
Which is clearly different to the default method as shown.
printr
PackageAs an aside, Yihui Xie also develops the printr
whose only purpose is to define several methods for this generic that can be applied to various outputs including help files called by ?lm
syntax.
It is definitely worth checking out before creating one of the methods yourself.
Since we are always dealing with lists, the obvious step for us is to define a knit_print.list
method that also utilises knit_child
.
Here is a simplified version of jcutils::printer_tabset
, the full version attempts to allow this to work in .Rmd and Quarto files.
printer_tabset <- function(x, options, ...) {
if (is.null(names(x))) names(x) <- seq_along(x)
header <- ":::: {.panel-tabset}"
footer <- "::::"
res <- lapply(seq_along(x), function(i) {
knitr::knit_child(
text = c(
"##### `r names(x)[i]`",
"",
"```{r}",
"#| echo: false",
"x[[i]]",
"```"
),
envir = environment(),
quiet = TRUE
)
})
out <- paste(c(header, res, footer), collapse = "\n\n")
knitr::asis_output(out)
}
Here is a line-by-line explanation of what this is doing for the curious.
Line 2. For an unnamed list, we assign sequential names as is standard for purrr
.
Lines 4 - 5. These are to surround the output to let Quarto know this is a tabset.
Lines 7 - 20. Looping over names and objects, knit_child
on each element, making sure to pass the environment as to avoid a 'x' not found
type of error.
Lines 21 - 22. Instead of printing, we return our object with the class knit_asis
so knitr can put this into the markdown file unaltered by code output like [1] ...
.
The beauty of this method is that knit_print
is also called within the child text and our method is passed to it. So we get nesting automatically!
In my workflow, I will include a registerS3method
(usually in the setup chunk) to be applied to all lists. This can be disabled on a per-chunk basis by supplying a chunk option render !expr knitr::normal_print
as required.
Using jcutils
, this would be
But for this blog, we can use
For more examples, including gt
see the in-progress vignette at the jcutils
GitHub page.
In this post though, suppose we make a nested list of visualisations of the Palmer penguins dataset.
data("penguins", package = "palmerpenguins")
# Used to create a ggplot object
gg_method <- function(.island, .x) {
penguins %>%
filter(.data$island == .island) %>%
ggplot(aes(x = .data[[.x]], y = .data$body_mass_g)) +
geom_point(na.rm = TRUE) +
labs(y = "Body Mass (g)", x = .x)
}
# Create a tibble of all combinations, with added plot column
plot_tb <-
expand_grid(
island = unique(penguins$island),
x = c("bill_length_mm", "bill_depth_mm", "flipper_length_mm")
) %>%
mutate(plot = Map(gg_method, .data$island, .data$x))
# Convert tibble to nested list using `split` twice
nested_list <-
split(plot_tb, plot_tb$island) %>%
map(~ split(.x, .x$x)) %>%
modify_depth(.depth = 2, ~ .x$plot[[1]])
Here, the top top-level of the list details the island variable,
And the second level indicates the variable to be used on the x-axis,
To print a list of plots, making the object visible creates expected tabsets.
I think the real advantage comes from nested lists, which I unfortunately couldn’t get working in .Rmd files, but works fine for Quarto formats like this blog!
Even better than this is the fact that each sub-list is treated independently and so ragged lists (lists of different lengths) will also work.
Josh Cowley. October 25th, 2022. “Quayside Stone Sculptures, Newcastle Upon Tyne”.