0

I would like to automatically generate R markdown chunks and headings within a for loop in R for a list of data.frames which varies length depending on input. These are then the input for plot generation. This R markdown document will then be knitted to a html_document using the package knitr.

I have attempted the solutions from several similar SO questions which can get me close, but is not what I actually need:

The problem is that I have 4 chunks that I'd like to repeat where each chunk has to have different chunk options (fig.width, fig.height) to correctly display the ggplot2 objects in them. So, I can't simply just supply a single set of options in the code chunk itself).

Here is an example of one of my failed attempts (NOTE: the second code block below also contains a comment indicating the chunk options which is required):

# Chunk data demo
data_list <- list(a=data.frame(), b=data.frame(), c=data.frame())
plot_1 <- list()
plot_2 <- list()

for (i in 1:length(data_list)) {
  data_list[[i]] <- data.frame(x = 1:5, y = 1:5)
  plot_1[[i]] <- ggplot(data_list[[i]], aes(x, y)) + geom_point()
  plot_2[[i]] <- ggplot(data_list[[i]], aes(x, y)) + geom_point()
}
#```{r, results='asis'} (Start of R markdown chunk)

# Chunk generation (only 2 chunks are written for brevity)   
for (i in 1:length(data_list)) {
  # Section heading
  cat("\n\n")
  cat("##", names(data_list)[i])

  # Chunk 1 ----
  cat("\n\n")
  cat("### Sub-section 1")
  cat("\n\n")
  cat("**Title 1**")
  cat("\n\n")
  cat('```{r, fig.width=10, fig.height=10}')
  cat("\n\n")
  print(plot_1[[i]])
  cat("\n\n")
  cat('```')
    
  # Chunk 2 ----
  cat("\n\n")
  cat("### Sub-section 2")
  cat("\n\n")
  cat("**Title 2**")
  cat("\n\n")
  cat('```{r, fig.width=10, fig.height=3}') # NOTE: Change in option
  cat("\n\n")
  print(plot_2[[i]])
  cat("\n\n")
  cat('```')
  cat("\n\n")
}
#``` (End of R markdown chunk)

EDIT: Removed the ggsave() calls, as not necessary to illustrate the problem which is apparent in the knitted document.

Julian
  • 6,586
  • 2
  • 9
  • 33
Buzz B
  • 75
  • 7
  • Those lines `ggsave` etc. don't appear to be put into the document: they are being run in the loop, and nothing in the document you are producing includes the files they produce. – user2554330 Feb 11 '23 at 09:56
  • I'm not sure I understand your comment, but the plots are created in an earlier chunk in the document. They are also produced in a `for` loop and are appended to a list with each iteration. They are then printed in the chunk as you see above (following the example here: https://bookdown.org/yihui/rmarkdown-cookbook/results-asis.html). The `ggsave()` is only there for me to check the output, which works as the files are produced. But as I outlined in the question, the problem is that I can't set different chunk options for multiple chunks. If there is a different approach I'd like to know. – Buzz B Feb 11 '23 at 23:05
  • 1
    I must have misunderstood what you are doing. But since it is incomplete, I can't just try it. So please simplify it and post a complete, reproducible example. – user2554330 Feb 11 '23 at 23:07
  • @user2554330 thank you for attempting to help. I have modified and added code which can demonstrate my attempt. – Buzz B Feb 12 '23 at 02:27

2 Answers2

1

I see what you are trying to do, and I think the approach you are taking won't work. (But I guess you already knew that!) Here's why:

When you run the asis chunk, you are producing Markdown, not R Markdown. The fig.width and fig.height options need to be handled by knitr when it is processing R Markdown, but that only happens once. The next stage of processing which handles the Markdown is done by Pandoc, and it doesn't know anything about fig.width and fig.height.

So what you need to do is to produce a "child document", then get knitr to include it in your main document. Those are described here: https://bookdown.org/yihui/rmarkdown-cookbook/child-document.html . Basically they are just fragments of R Markdown to include in your main document. Here's a modification of your document that does this:

---
title: "Untitled"
author: "Duncan Murdoch"
date: "2023-02-12"
output: 
  html_document:
    keep_md: true
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```

## R Markdown

```{r}
library(ggplot2)
# Create the plots
data_list <- list(a=data.frame(), b=data.frame(), c=data.frame())
plot_1 <- list()
plot_2 <- list()

for (i in 1:length(data_list)) {
  data_list[[i]] <- data.frame(x = 1:5, y = 1:5)
  plot_1[[i]] <- ggplot(data_list[[i]], aes(x, y)) + geom_point()
  plot_2[[i]] <- ggplot(data_list[[i]], aes(x, y)) + geom_point()
}
```

```{r echo = FALSE}
# Create a child document
child <- "child.Rmd"
cat("\n", file = child)

# Save typing file = child, append = TRUE every time
# by making a new function to call cat

cat2 <- function(...) cat(..., file = child, append = TRUE)

for (i in 1:length(data_list)) {
  # Section heading
  cat2("\n\n")
  cat2("##", names(data_list)[i])

  # Chunk 1 ----
  cat2("\n\n")
  cat2("### Sub-section 1")
  cat2("\n\n")
  cat2("**Title 1**")
  cat2("\n\n")
  cat2('```{r fig.width=10, fig.height=10}')
  cat2("\n\n")
  cat2("i = ", i, "\n")
  cat2("print(plot_1[[i]])")
  cat2("\n\n")
  cat2('```')
    
  # Chunk 2 ----
  cat2("\n\n")
  cat2("### Sub-section 2")
  cat2("\n\n")
  cat2("**Title 2**")
  cat2("\n\n")
  cat2("```{r fig.width=10, fig.height=3}")
  cat2("\n\n")
  cat2("print(plot_2[[i]])")
  cat2("\n\n")
  cat2('```')
  cat2("\n\n")
}
```

```{r child="child.Rmd"}
```
user2554330
  • 37,248
  • 4
  • 43
  • 90
  • Thank you, but this method doesn't fully work as the plots for each iteration of the `for` loop are all the same (from the last iteration) in the knitted document. – Buzz B Feb 12 '23 at 20:17
  • 1
    Right, you can't refer to `i` after the loop is done. You need to add `cat2("i =", i)` before the `cat2("print(plot_1[[i]]")` line. – user2554330 Feb 12 '23 at 20:24
  • Thank you, this was very much appreciated. I added `cat2("i <-", i)` in its own code chunk above the `# Section heading` comment since the section headings also refer to `i`. The method does leave the `"child.Rmd"` behind, so I wish it wouldn't need to create a document, but manually deleting it is no big deal. – Buzz B Feb 15 '23 at 16:27
  • I think you could have a final chunk in the main document containing `unlink("child.Rmd")` to do the cleanup, or put the file in `tempdir()` for automatic cleanup. – user2554330 Feb 15 '23 at 17:08
1

You can also use the knitr::knit_expand() and pass it to the knitr::knit_child() function.

Edit:

---
output: html_document
---


```{r, echo=FALSE, results='asis'}
library(purrr)
library(ggplot2)
data_list <- list(a=data.frame(x = 1:5, y = 1:5), b=data.frame(x = 1:5, y = 1:10),  c=data.frame(x = 1:5, y = 1:15))


data_plots_1 <- map(data_list, ~ggplot(.x, aes(x, y)) + geom_point())
data_plots_2 <- map(data_list, ~ggplot(.x, aes(x, y)) + geom_point(color = "red"))

fig_width <- c(7,5,5)
fig_height <- c(5,3,3)

data_names <- data_list %>% names()
```



```{r results='asis', echo=FALSE}
structure_text <- purrr::pmap_chr(list(data_names, fig_width, fig_height), \(data_names,  fig_width, fig_height) {
  knitr::knit_expand(text = c(
    "## Title Plot 1 dataset {{data_names}}", 
    "",
    "",
    "```{r, fig.width= {{ fig_width }},  fig.height= {{ fig_height }}, echo =FALSE }",
    "data_plots_1${{data_names}} ",
    "```",
    "",
    "## Title Plot 2 dataset {{data_names}}", 
    "",
    "",
    "```{r, fig.width= {{ fig_width }},  fig.height= {{ fig_height }}, echo =FALSE }",
    "data_plots_2${{data_names}} ",
    "```",
    ""
    ))
})
res <- knitr::knit_child(text = structure_text, quiet = TRUE)
cat(res, sep = '\n')

enter image description here

Old

---
output: html_document
---


```{r, echo=FALSE, results='asis'}
library(purrr)
library(ggplot2)
data_list <- list(a=data.frame(x = 1:5, y = 1:5), b=data.frame(x = 1:5, y = 1:10))
data_names <- data_list %>% names()
fig_width <- c(7,5)
fig_height <- c(5,3)
```



```{r results='asis', echo=FALSE}
structure_text <- purrr::pmap_chr(list(data_names, fig_width, fig_height), \(data_names, fig_width, fig_height) {
  knitr::knit_expand(text = c(
    "## {{ data_names }}", 
    "",
    "",
    "```{r, fig.width= {{ fig_width }},  fig.height= {{ fig_height }}, echo =FALSE }",
    "data_list${{data_names}}  |>",
    "  ggplot(aes(x = x, y = y)) +",
    "  geom_point()",
    "```",
    ""))
})
res <- knitr::knit_child(text = structure_text, quiet = TRUE)
cat(res, sep = '\n')

enter image description here

Julian
  • 6,586
  • 2
  • 9
  • 33
  • Thank you for your help, but implementing this doesn't work: `Error in setwd(opts_knit$get("output.dir")) : character argument expected`. There is a difference in my code to yours: you are constructing the `ggplot` plots in the chunk, whereas I have already created them and am trying to print the objects (it's not feasible for me construct the graphs here as they are complex arrangements built with `cowplot()` and the subplots are included conditionally. – Buzz B Feb 12 '23 at 22:24
  • 1
    I made an edit, let me know if it works now – Julian Feb 13 '23 at 09:22
  • Thank you for your help. I managed to get this method to work by adding at the beginning of the code chunk containing the `structure_text`: `opts_knit$set(output.dir=getwd())`, as I kept running into an error (see previous comment). – Buzz B Feb 15 '23 at 16:31