2

In an rmarkdown document, I'm plotting several plots inside a single chunk. I'd like to make the height of each plot proportional to the number of categories on the vertical axis. Based on this SO answer, I'm trying to do this using knit_expand to create a new chunk for each plot with the chunk options set programmatically.

A sample rmarkdown document with reproducible data and code is pasted in below. The kexpand function is what creates a new chunk for each plot. I run the function in a for loop in the last chunk. The for loop creates three plots, one for each level of dept in the sample data. However, when I (attempt to) knit the document, I get the following error:

Line 23 Error in parse_block(g[-1], g[1], params.src) : duplicate label 'English' Calls: ... process_file -> split_file -> lapply -> FUN -> parse_block Execution halted

This is the first time I've tried to dynamically change chunk options and I'm not sure how a chunk label is getting duplicated, since each level of dept goes into knit_expand only once. I tried the suggestion in this SO question to use knit_child instead of knit inside the kexpand function, but I got the same error.

So, my questions are: (1) how can I avoid the duplicate label error and get the document to knit properly, and (2) am I going about this the right way or is there a better way to dynamically change chunk options when creating plots or tables within a loop?

---
title: "Untitled"
author: "eipi10"
date: "February 12, 2017"
output: pdf_document
---

```{r setup, include=FALSE}
knitr::opts_knit$set(progress = FALSE, verbose = FALSE)
knitr::opts_chunk$set(echo = FALSE, message=FALSE, warning=FALSE)

library(tidyverse)
library(scales)
library(knitr)
```

```{r}
# Adapted from https://stackoverflow.com/a/27234462/496488
kexpand <- function(ht, cap, plot) {
  cat(
    knit(
      text=knit_expand(
        text=sprintf("```{r %s, fig.height=%s, fig.cap='%s'}\n%s\n```", cap, ht, cap, plot)
      )))
}
```

```{r data}
df = structure(list(dept = c("English", "English", "English", "English", 
"English", "English", "English", "English", "English", "English", 
"English", "English", "English", "English", "English", "Biology", 
"Biology", "Biology", "Biology", "Biology", "Biology", "Government", 
"Government"), tot_enrl = c(114, 349, 325, 393, 415, 401, 166, 
117, 302, 267, 256, 224, 481, 295, 122, 410, 478, 116, 278, 279, 
238, 142, 145), course = c("ENGL 1", "ENGL 10M", "ENGL 11M", 
"ENGL 16", "ENGL 1X", "ENGL 20M", "ENGL 3", "ENGL 30A", "ENGL 40A", 
"ENGL 40B", "ENGL 50A", "ENGL 50B", "ENGL 5M", "ENGL 60", "ENGL 65", 
"BIO 15L", "BIO 2", "BIO 30", "BIO 39", "BIO 7", "BIO 9", "GOVT 10", 
"GOVT 1H")), class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA, 
-23L), .Names = c("dept", "tot_enrl", "course"))
```

```{r results="asis"}
height.unit = 0.2

for (d in unique(df$dept)) {

  .pl = ggplot(df[df$dept==d, ], aes(tot_enrl, reorder(course,tot_enrl))) + geom_point()

  ht = height.unit *  length(unique(df$course[df$dept==d])) + 1

  kexpand(ht=ht, cap=d, plot=.pl)
}
```
Community
  • 1
  • 1
eipi10
  • 91,525
  • 24
  • 209
  • 285

2 Answers2

2

With a small modification (check the definition of kexpand), your example code works for me:

---
title: "Untitled"
author: "eipi10"
date: "February 12, 2017"
output: pdf_document
---

```{r setup, include=FALSE}
knitr::opts_knit$set(progress = FALSE, verbose = FALSE)
knitr::opts_chunk$set(echo = FALSE, message=FALSE, warning=FALSE)

library(tidyverse)
library(scales)
library(knitr)
```

```{r}
# Adapted from http://stackoverflow.com/a/27234462/496488
kexpand <- function(ht, cap) {
  cat(
    knit(
      text=knit_expand(
        text=sprintf("```{r %s, fig.height=%s, fig.cap='%s'}\n .pl \n```", cap, ht, cap)
      )))
}
```

```{r data}
df = structure(list(dept = c("English", "English", "English", "English", 
"English", "English", "English", "English", "English", "English", 
"English", "English", "English", "English", "English", "Biology", 
"Biology", "Biology", "Biology", "Biology", "Biology", "Government", 
"Government"), tot_enrl = c(114, 349, 325, 393, 415, 401, 166, 
117, 302, 267, 256, 224, 481, 295, 122, 410, 478, 116, 278, 279, 
238, 142, 145), course = c("ENGL 1", "ENGL 10M", "ENGL 11M", 
"ENGL 16", "ENGL 1X", "ENGL 20M", "ENGL 3", "ENGL 30A", "ENGL 40A", 
"ENGL 40B", "ENGL 50A", "ENGL 50B", "ENGL 5M", "ENGL 60", "ENGL 65", 
"BIO 15L", "BIO 2", "BIO 30", "BIO 39", "BIO 7", "BIO 9", "GOVT 10", 
"GOVT 1H")), class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA, 
-23L), .Names = c("dept", "tot_enrl", "course"))
```

```{r results="asis"}

height.unit = 0.2

for (d in unique(df$dept)) {

  .pl = ggplot(df[df$dept==d, ], aes(tot_enrl, reorder(course,tot_enrl))) + geom_point()

  ht = height.unit *  length(unique(df$course[df$dept==d])) + 1

  kexpand(ht=ht, cap=d)
}
```

enter image description here

Martin Schmelzer
  • 23,283
  • 6
  • 73
  • 98
2

Using knit_expand is the correct approach here, but the implementation you adapted from rawr's anser is … weird (IMHO).

This is at the core of your solution:

knit_expand(text=sprintf("```{r %s, fig.height=%s, fig.cap='%s'}\n%s\n```", cap, ht, cap, plot))

This may work in the context of the r-bloggers.com post rawr's solution is based on, but in general this is not the way knit_expand is supposed to be used.

  • knit_expand resolves templates on its own – using sprintf here is rather pointless. From ?knit_expand

    This function expands a template based on the R expressions in {{}} (this tag can be customized by the delim argument). These expressions are extracted, evaluated and replaced by their values in the original template.

  • Instead of injecting code into the dynamically generated chunk, you are / rawr is injecting .pl, the return value of ggplot.

Try running

knit_expand(text=sprintf("```{r %s, fig.height=%s, fig.cap='%s'}\n%s\n```", d, ht, d, .pl))

with the variables as defined in the code from the question (and d set to unique(df$dept)[1]). This expands to 9 chunks (and a lot of garbage), because of the way .pl is converted to character. The "duplicate label" errors stem from all of these chunks having the same label.

The correct solution should be similar to the last part in this answer: Use the templating capabilities of knit_expand and move the code generating the plot inside the generated chunks:

---
output: pdf_document
---

```{r setup, include=FALSE}
knitr::opts_knit$set(progress = FALSE, verbose = FALSE)
knitr::opts_chunk$set(echo = FALSE, message=FALSE, warning=FALSE)

library(ggplot2)
library(knitr)
```

```{r}
kexpand <- function(height, caption, d) {

  cat(
    knit(text=knit_expand(
      text=(
"```{r {{caption}}, fig.height={{height}}, fig.cap='{{caption}}'}
ggplot(df[df$dept=='{{d}}', ], aes(tot_enrl, reorder(course,tot_enrl))) + geom_point()
```"),
      caption = caption,
      height = height,
      d = d)))
}
```

```{r data}
df = structure(list(dept = c("English", "English", "English", "English", 
    "English", "English", "English", "English", "English", "English", 
    "English", "English", "English", "English", "English", "Biology", 
    "Biology", "Biology", "Biology", "Biology", "Biology", "Government", 
    "Government"), tot_enrl = c(114, 349, 325, 393, 415, 401, 166, 
    117, 302, 267, 256, 224, 481, 295, 122, 410, 478, 116, 278, 279, 
    238, 142, 145), course = c("ENGL 1", "ENGL 10M", "ENGL 11M", 
    "ENGL 16", "ENGL 1X", "ENGL 20M", "ENGL 3", "ENGL 30A", "ENGL 40A", 
    "ENGL 40B", "ENGL 50A", "ENGL 50B", "ENGL 5M", "ENGL 60", "ENGL 65", 
    "BIO 15L", "BIO 2", "BIO 30", "BIO 39", "BIO 7", "BIO 9", "GOVT 10", 
    "GOVT 1H")), class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA, 
    -23L), .Names = c("dept", "tot_enrl", "course"))
```

```{r results="asis"}
height.unit <- 0.2

for (d in unique(df$dept)) {
  ht <- height.unit *  length(unique(df$course[df$dept==d])) + 1

  kexpand(height=ht, caption=d, d = d)
}
```
Community
  • 1
  • 1
CL.
  • 14,577
  • 5
  • 46
  • 73