Loops in Rmarkdown: How to make an in-text figure reference? Figure captions?

1.4k views Asked by At

{r setup, include=FALSE, message=FALSE, results="hide"} knitr::opts_chunk$set(echo = TRUE) library(knitr) library(kfigr) library(dplyr) library(png) library(grid) library(pander) library(ggplot2)

Question

Loops in rmarkdown: in-text figure reference? figure captions?

Goal

Use a for loop to create sections with text, in-text results, and multiple figure references with associated figure captions in the figure list. The figure references/numbering should be seemless with figures numbered before and after these new sections.

Note: The figures referenced in the for loop are generated earlier in the text, saved as pngs, and then re-loaded. This might seem clunky for the purpose of this example, but the actual figs are maps and are slow to generate (I plan to comment out the loop that generates the figures once I have them how I want).

{r echo = FALSE, warnings=FALSE, message=FALSE, results="hide"}

Data: Each year we have a different number of strata, hence the need for a loop.

df <- rbind(
  data.frame(strata = rep("A", 10), x = rnorm(10, mean= 10), y = rnorm(10, mean = 15),z = rnorm(10, mean = 20)),
  data.frame(strata = rep("B", 10), x = rnorm(10, mean= 5), y = rnorm(10, mean = 10), z = rnorm(10, mean = 15)),
  data.frame(strata = rep("C", 10), x = rnorm(10, mean= 15), y = rnorm(10, mean = 20), z = rnorm(10, mean = 10)))

first_plot: the figure that should appear in the list before for loop creates the sections by strata

first_plot <- ggplot(df, aes(x, fill=strata)) + geom_histogram()

last_plot: the figure that should appear in the list after the for loop creates the sections by strata

last_plot <- ggplot(df, aes(x = strata, y = z)) + geom_boxplot()

Figure generation (this is the part that will be commented out later in my version once I have the maps how I want)

strat <- unique(df$strata)

for (i in seq_along(strat)) {
  sub <- df %>% filter(strata %in% strat[i])
  fig1 <- ggplot(sub, aes(x = x, y = y)) + geom_point()
  ggsave(fig1, file=paste0("fig1_", strat[i], ".png"))
  fig2 <- ggplot(sub, aes(x = x, y = z)) + geom_point() 
  ggsave(fig2, file=paste0("fig2_", strat[i], ".png"))
}    

Load the png's

df_figs <- list.files(pattern = "\\.png$")
for (i in df_figs){  
   name <- gsub("-",".",i)
   name <- gsub(".png","",name)  
   i <- paste(".\\",i,sep="")
   assign(name,readPNG(i))
}

Introduction section

Some introductory text in the report and a figure r figr('first_plot',TRUE, type='Figure').

```{r echo = FALSE, warnings=FALSE, message=FALSE, results = "asis"}

# Summary of results and image file names that will be references in text
results <- df %>% 
  group_by(strata) %>% 
  dplyr::summarise_each(funs(mean)) %>% 
  mutate(fig1 = paste0("fig1_", strata),
         fig2 = paste0("fig2_", strata))

#Text template (each strata will have its own section)
template <- "# The %s stratum
The mean of *x* in %s stratum was %1.1f. Relationships between *x* and *y* and *x* and *z* can be found in `r figr('%s', TRUE, type='Figure')` and `r figr('%s', TRUE, type='Figure')`.

"

#Create markdown sections in for loop
for(i in seq(nrow(results))) {
  current <- results[i, ]
  cat(sprintf(template, 
              current$strata, current$strata, 
              current$x, 
              current$fig1, current$fig2))
}

#Also doesn't work:
template <- "# The %s stratum
The mean in %s stratum was %1.0f. Results can be found in "
template2 <- " and "
template3 <- ".

"

`figr('%s', TRUE, type='Figure')` and `figr('%s', TRUE, type='Figure')`."

#For loop
for(i in seq(nrow(results))) {
  current <- results[i, ]
  cat(sprintf(template,
            current$strata, current$strata,
            current$mean,
            current$fig_1, current$fig_2))
  print(paste0("`r figr(",paste0("'", current$fig1,"'"), TRUE, type='Figure'))
  cat(sprintf(template2))
  print(paste0("`r figr(",paste0("'", current$fig2,"'"), "TRUE, type='Figure'),`"))
  cat(sprintf(template3))
 }
```

Conclusion section

Some discussion text in the report and figure r figr('last_plot',TRUE, type='Figure').

Figures

*NOTE:* I don't know how to automate the looped portion of the list of figures here, so I've done it by hand.

```{r 'first_plot', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="The caption for the first figure."}
suppressMessages(print(first_plot))
```

```{r 'fig1_A', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="Caption text for fig1_A."}
grid.raster(fig1_A)
```

```{r 'fig2_A', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="Caption text for fig2_A."}
grid.raster(fig2_A)
```

```{r 'fig1_B', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="Caption text for fig1_B."}
grid.raster(fig1_B)
```

```{r 'fig2_B', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="Caption text for fig2_B."}
grid.raster(fig2_B)
```

```{r 'fig1_C', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="Caption text for fig1_C."}
grid.raster(fig1_C)
```

```{r 'fig2_C', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="Caption text for fig2_C."}
grid.raster(fig2_C)
```

```{r 'last_plot', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6,  fig.cap="The caption for the last figure."}
suppressMessages(print(last_plot))
```
1

There are 1 answers

0
sullij On BEST ANSWER

SOLUTIONS

  1. Use knit_expand()
  2. Use captioner instead of kfigr
  3. This numbers your figures (or tables) in text and at the end of your report.
  4. This script shows you how to create markdown paragraphs in for loops that have in-text references to figures.
  5. It also shows you how to create custom figure captions in for loops while retaining the number order.
  6. If you show how to do #4 and #5 using brew I will give you all the SO points.

Libraries

library(knitr) library(dplyr) library(png) library(grid) library(pander) library(ggplot2) library(devtools) library(captioner)

Create a fig_nums() function using the captioner package (https://github.com/adletaw/captioner/blob/master/vignettes/using_captioner.Rmd)

fig_nums <- captioner(prefix = "Figure")

Data

Each year we have a different number of strata, hence the need for a loop.

df <- rbind(
  data.frame(strata = rep("A", 10), x = rnorm(10, mean= 10), y = rnorm(10, mean = 15), z = rnorm(10, mean = 20)),
  data.frame(strata = rep("B", 10), x = rnorm(10, mean= 5), y = rnorm(10, mean = 10), z = rnorm(10, mean = 15)),
  data.frame(strata = rep("C", 10), x = rnorm(10, mean= 15), y = rnorm(10, mean = 20), z = rnorm(10, mean = 10)))

first_plot: the figure that should appear in the list before for loop creates the sections by strata

first_plot <- ggplot(df, aes(x, fill=strata)) + geom_histogram()
fig_nums("first_plot", display = FALSE)

last_plot: the figure that should appear in the list after the for loop creates the sections by strata

last_plot <- ggplot(df, aes(x = strata, y = z)) + geom_boxplot()

Figure generation

Comment this section out once you have figs how you want. This step will not feel convoluted, unnatural, suboptimal, unnecessary, or like a very bad idea if you do a lot of mapping in R.

strat <- unique(df$strata)

  for (i in seq_along(strat)) {
   sub <- df %>% filter(strata %in% strat[i])
   fig1 <- ggplot(sub, aes(x = x, y = y)) + geom_point()
   ggsave(fig1, file=paste0("fig1_", strat[i], ".png"))
   fig2 <- ggplot(sub, aes(x = x, y = z)) + geom_point() 
   ggsave(fig2, file=paste0("fig2_", strat[i], ".png"))
 }        

Load the png's

df_figs <- list.files(pattern = "\\.png$")
for (i in df_figs){  
   name <- gsub("-",".",i)
   name <- gsub(".png","",name)  
   i <- paste(".\\",i,sep="")
   assign(name,readPNG(i))
}

Introduction

Some introductory text in the report and a figure r fig_nums("first_plot", display="cite").

Results and image file names that will be referenced in text:

```{r echo = FALSE, warnings=FALSE, message=FALSE, results = "asis"}

    results <- df %>% 
      group_by(strata) %>% 
      dplyr::summarise_each(funs(mean)) %>% 
      mutate(fig1 = paste0("fig1_", strata),
             fig2 = paste0("fig2_", strata))

```

```{r run-numeric-md, warning=FALSE, include=FALSE}

#The text for the markdown sections in for loop... the knit_expand() is the work-horse here.

out = NULL
for (i in as.character(unique(results$strata))) {
  out = c(out, knit_expand(text=c('#### The *{{i}}* strata',
                                  '\n',
                                  'The mean of *x* is ',
                                  '{{paste(sprintf("%1.1f", results$x[results$strata==i]))}}', '({{fig_nums(results$fig1[results$strata==i],display="cite")}}).',
                                  '\n'
  )))
}

```

Creates section for each strata

`r paste(knit(text = out), collapse = '\n')`

Conclusion

Some discussion text in the report and figure r fig_nums("last_plot",display="cite").

List of Figures

`r fig_nums("first_plot",caption="Here is the caption for the first figure.")`

```{r 'first_plot', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6}
suppressMessages(print(first_plot))
```

```{r figcaps, include=FALSE}
caps = NULL

for (i in as.character(unique(results$strata))) {
  caps = c(caps, knit_expand(  
    text=c({{fig_nums(results$fig1[results$strata==i], caption="Caption text for strata *{{i}}* goes here.")}},
           '``` {r {{results$fig1[results$strata==i]}}, echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6}',
           {{paste0('grid.raster(',results$fig1[results$strata==i],')')}},
                                    '```',
           '\n')))
}

#DON'T FORGET TO UNLIST!
src <- unlist(caps)
```

`r paste(knit(text = src),sep='\n')`

`r fig_nums("last_plot", caption="The caption for the last figure.")`

```{r 'last_plot', echo=FALSE, warning=FALSE, fig.width=6.5, fig.height=6}
suppressMessages(print(last_plot))
```