class: center, middle, inverse, title-slide # Programmatically creating tabs in Rmarkdown ## Monday Munch ### Jonathan Jayes ###
05 March, 2022
--- class: inverse, center, middle # When to write code that writes its own code --- # Plan .pull-left[ 1. [What is the problem?](#problem) 2. [What are the tools to solve it?](#tools) 3. [How does it work?](#work) ] .pull-right[ <img src="images/cover.jpg" width="853" style="display: block; margin: auto;" /> ] --- name: problem # Problem .pull-left[ I want to create a parametrized report in Rmarkdown. The report is populated after querying a database. I want to have tabs in the report that display a graph for subsets of the data. The number of subsets depends on the result of the query. We simulate the problem by randomly removing one of the islands from the `palmerpenguins` dataset. ] .pull-right[ ```r df <- palmerpenguins::penguins df %>% distinct(island) %>% knitr::kable() ``` |island | |:---------| |Torgersen | |Biscoe | |Dream | ```r island_subsample <- df %>% distinct(island) %>% slice_sample(n = 2) df <- df %>% filter(island %in% island_subsample$island) ``` ] --- # Problem If I hardcode the number of tabs: .panelset[ .panel[.panel-name[Biscoe] <img src="index_files/figure-html/unnamed-chunk-6-1.svg" style="display: block; margin: auto;" /> ] .panel[.panel-name[Dream] <img src="index_files/figure-html/unnamed-chunk-7-1.svg" style="display: block; margin: auto;" /> ] .panel[.panel-name[Torgersen] <img src="index_files/figure-html/unnamed-chunk-8-1.svg" style="display: block; margin: auto;" /> ] ] One of the plots is empty! --- # Bad solutions - Amend my `plot_penguins` function to not plot if there is no data: ```r plot_penguins <- function(isl) { * if (dim(df %>% filter(island == isl))[1] != 0) { df %>% filter(island == isl) %>% ggplot(aes(bill_length_mm, bill_depth_mm, colour = body_mass_g, shape = species)) + geom_point(cex = 2) + geom_smooth(method = "lm", se = F) + scale_color_viridis_c(labels = scales::number_format()) + labs( x = "Bill length (mm)", y = "Bill depth (mm)", colour = "Body mass (g)", shape = "Penguin species" ) * } } ``` - Others? --- class: inverse, center, middle # Better solution ### Generate the tabs programmatically, with code that writes itself depending on the contents of the returned dataset. --- name: tools # Tools I am creating the tabs with [Garrick Aden-Buie's](https://www.garrickadenbuie.com/) `xaringanExtra` package. <iframe src="https://pkg.garrickadenbuie.com/xaringanExtra/panelset/?panelset=r-code#2" width="100%" height="400px" data-external="1"></iframe> Other useful sources incude [this Science Loft post](https://scienceloft.com/technical/programmatically-create-new-headings-and-outputs-in-rmarkdown/) and [this Stack Overflow question](https://stackoverflow.com/questions/63397427/programmatically-create-tab-and-plot-in-markdown). --- # Tools In vanilla Rmarkdown you can create the tabs with a simple heading structure. ```r ## Here are the penguins {.panelset} ### Biscoe plot_penguins("Biscoe") ### Dream plot_penguins("Dream") ``` -- In my report however I do not want the end of the tabset to be delineated only by another second level heading: ```r *## Here are the penguins {.panelset} ### Biscoe plot_penguins("Biscoe") ### Dream plot_penguins("Dream") *## More about the penguins ``` --- # Tools Instead I opt to use [Pandoc's fenced div structure](https://pandoc.org/MANUAL.html#extension-fenced_divs), which looks like this: ```r ::::: {.panelset} ::: {.panel} [Biscoe]{.panel-name} plot_penguins("Biscoe") ::: ::: {.panel} [Dream]{.panel-name} plot_penguins("Dream") ::: ::::: ``` --- name: work # How does it work? Now I can programmatically create my tabs and plots within the fences: ```r # function to make the tabs make_tab <- function(isl) { cat("::: {.panel}\n") cat("##", isl, "{.panel-name}\n") cat("Plot of", isl, "\n") p <- plot_penguins(isl) print(p) cat("\n") cat(":::\n") } ::::: {.panelset} # get a list of islands present in the dataset isl_list <- unique(df$island) %>% as.character() # make the tabs for each island that is there for (i in isl_list){ make_tab(i) } ::::: ``` --- # To remember - Wrap code you want executed in `cat()` and `print()` your ggplot objects. - Set code options to `results = 'asis'` for the code chunk that makes your tabs, according to the [Rmarkdown Cookbook](https://bookdown.org/yihui/rmarkdown-cookbook/results-asis.html): <blockquote> This option tells knitr not to wrap your text output in verbatim code blocks, but treat it “as is.” This can be particularly useful when you want to generate content dynamically from R code. </blockquote> - Put in lots of line breaks to separate Rmarkdown content, e.g. frequent uses of `cat("\n")` between elements. --- class: inverse, center, middle # Does it work? --- # Yes! .panelset[ .panel[.panel-name[Biscoe] <img src="index_files/figure-html/unnamed-chunk-16-1.svg" style="display: block; margin: auto;" /> ] .panel[.panel-name[Dream] <img src="index_files/figure-html/unnamed-chunk-16-2.svg" style="display: block; margin: auto;" /> ] ] --- # Yes* For the `xaringan` slides we must use the `.panelset[...]` notation: ```r make_tab_xaringan <- function(isl) { cat(".panel[.panel-name[", isl, "]\n", sep = "") p <- plot_penguins(isl) print(p) cat("\n") cat("]\n") } .panelset[ isl_list <- unique(df$island) %>% as.character() for (i in isl_list){ make_tab_xaringan(i) } ] ``` But in Rmarkdown parametrized reports the code on [slide 11](#work) works very well! --- class: inverse, center, middle # Thank you