12

I want to be able to print out non-predetermined list of plots in knitr. I am able to do that, but there are a few wrinkles left to iron out. Namely:

1) How do you suppress the list indices (like [[2]]) on each page that precede each plot? Using echo=FALSE doesn't do anything.

2) Is it possible to set the size for each plot as they are rendered? I've tried setting a size variable outside of the chunk, but that only lets me use one value and not a different value per plot.

I'm asking this as one question because they seem to speak to the same lesson, i.e., producing a list of plots.

Some sample code:

\documentclass{article}
\usepackage[margin=.5in, landscape]{geometry}
\begin{document}

<<diamond_plots, echo = FALSE, results = 'hide'>>==
library(ggplot2)

diamond_plot = function(data, cut_type){
  ggplot(data, aes(color, fill=cut)) + 
    geom_bar() +
    ggtitle(paste("Cut:", cut_type, sep = ""))
}

cuts = unique(diamonds$cut)
plots = list()
for(i in 1:length(cuts)){
    data = subset(diamonds, cut == cuts[i])
    plots[[i]] = diamond_plot(data, cuts[i])
}
height = 3
@

<<print_plots, results='asis', echo=FALSE,  fig.width=10, fig.height=height>>=
plots
@
\end{document}

The PDF of the plots looks like this:

enter image description here

Sam Firke
  • 21,571
  • 9
  • 87
  • 105
Nancy
  • 3,989
  • 5
  • 31
  • 49

2 Answers2

18

1) How do you suppress the list indices (like [[2]]) on each page that precede each plot? Using echo=FALSE doesn't do anything.

Plot each element in the list separately (lapply) and hide the output from lapply (invisible):

invisible(lapply(plots, print))

2) Is it possible to set the size for each plot as they are rendered? I've tried setting a size variable outside of the chunk, but that only let's me use one value and not a different value per plot.

Yes. In general, when you pass vectors to figure-related chunk options, the ith element is used for the ith plot. This applies to options that are "figure specific" like e.g. fig.cap, fig.scap, out.width and out.height.

However, other figure options are "device specific". To understand this, it is important to take a look at the option dev:

dev: the function name which will be used as a graphical device to record plots […] the options dev, fig.ext, fig.width, fig.height and dpi can be vectors (shorter ones will be recycled), e.g. <<foo, dev=c('pdf', 'png')>>= creates two files for the same plot: foo.pdf and foo.png

While passing a vector to the "figure specific" option out.height has the consequence of the ith element being used for the ith plot, passing a vector to the "device specific" option has the consequence of the ith element being used for the ith device.

Therefore, generating dynamically-sized plots needs some hacking on chunks because one chunk cannot generate plots with different fig.height settings. The following solution is based on the knitr example `075-knit-expand.Rnw and this post on r-bloggers.com (which explains this answer on SO).

The idea of the solution is to use a chunk template and expand the template values with the appropriate expressions to generate chunks that, in turn, generate the plots with the right fig.height setting. The expanded template is passed to knit in order to evaluate the chunk:

\documentclass{article}
\begin{document}

<<diamond_plots, echo = FALSE, results = "asis">>==
library(ggplot2)
library(knitr)

diamond_plot = function(data, cut_type){
  ggplot(data, aes(color, fill=cut)) +
    geom_bar() +
    ggtitle(paste("Cut:", cut_type, sep = ""))
}

cuts = unique(diamonds$cut)

template <- "<<plot-cut-{{i}}, fig.height = {{height}}, echo = FALSE>>=
    data = subset(diamonds, cut == cuts[i])
    plot(diamond_plot(data, cuts[i]))
@"

for (i in seq_along(cuts)) {
  cat(knit(text = knit_expand(text = template, i = i, height = 2 * i), quiet = TRUE))
}

@

\end{document}

The template is expanded using knit_expand which replaces the expressions in {{}} by the respective values.

For the knit call, it is important to use quite = TRUE. Otherwise, knit would pollute the main document with log information.

Using cat is important to avoid the implicit print that would deface the output otherwise. For the same reason, the "outer" chunk (diamond_plots) uses results = "asis".

Community
  • 1
  • 1
CL.
  • 14,577
  • 5
  • 46
  • 73
  • Upvoted and accepted, though I'll note (as you did, too!) that I am seeing substantial distortion because of out.height vs fig.height. If I/someone can figure out how to get around the distortion that would be stellar-- I'll report back if I figure it out. – Nancy Nov 16 '15 at 20:13
  • The error I get when I try to set fig.height as a variable is "Error in options[[sprintf("fig.%s", i)]] * options$dpi : " – Nancy Nov 16 '15 at 20:21
  • This error message should occur only if you pass something like `paste0(seq_along(cuts)*4, "cm")` to `fig.height`. This is because `fig.height` must be numeric; `fig.height=seq_along(cuts)` *should* work (IMHO), but does nothing. – CL. Nov 17 '15 at 08:15
  • I got that @user2706569 I just wanted to add the error message in case someone could use it to help us use fig.height with a variable instead of out.height. – Nancy Nov 17 '15 at 16:25
  • 1
    I updated the answer; the new solution might be more useful … – CL. Nov 17 '15 at 18:39
  • Solid! Works on my machine; I'll try it on my more involved project, but am optimistic. Great work! – Nancy Nov 17 '15 at 22:46
  • This is indeed excellent, thank you @CL.! Note for others: you *have* to provide unique chunk names in the template for this to work (at least for me), otherwise R will complain about multiple instances of "unnamed chunk 1". – Matherion Mar 12 '17 at 12:31
  • @CL. that `invisible` function works great! I needed to plot multiple plots in my `knitr` report for similar data at different locations and this works perfectly. I added a generic caption underneath and the `ggplot` legend created through `aes` explains the specifics. – Prevost Nov 12 '18 at 16:50
0

You need to access the individual list elements, otherwise print will always print out the indices.

Not sure if it is the cleanest answer but you can just print them over a loop.

> print(plots)
[[1]]

[[2]]

[[3]]

[[4]]

[[5]]

> for(x in plots){print(x)}

But I didn't try this in tex, just the console.

intra
  • 366
  • 1
  • 2
  • 10
  • Oddly, the indices still show up. I vaguely remember having tried that before and being surprised. Any thoughts on dynamically changing fig.height? – Nancy Nov 13 '15 at 20:59
  • Not fun thoughts. I do most of my output in markdown and I have to place the html myself if I want to automate the size of a plot because you can't pass parameters to that knitr block. If you only have 5 plots, do it manually (individual r code blocks). Otherwise, do one, take a look at the raw latex, and copy that, changing the parameters you need to and then inject that directly using r code. I believe the 'asis' flag allows to you pass latex output directly from an r function – intra Nov 13 '15 at 21:06
  • Hm interesting. Unfortunately I'm trying to write for an application that will be fully automated with varying inputs. I might just set the figure size to some constant and then slice the data to fit the figure size as opposed to the inverse. – Nancy Nov 13 '15 at 22:03