R Markdownのコードチャンク中でMarkdownを生成するときの注意点

R Markdownコードチャンク内でMarkdownを生成できたら良い、と思う場面はたまにあります。（`cat()`で出力すればいいんでしょ簡単簡単）と思ってたら割と色々なところでハマってしまったので注意点をメモっておきます。

### そもそも出力させる

まず、出力がそのままMarkdownに反映されないといけないので、コードチャンクのオプションでは`results='asis'`を指定するようにします。

あとは`cat()`や`print()`で記述してけばOKです。

```r:.Rmd
cat("### lv 3 header")
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/109348/92b641c3-f106-95b2-4508-2dfd7c302029.png)


### 見出しを出力するときの注意

[Pandoc - Pandoc User’s Guide](https://pandoc.org/MANUAL.html#pandocs-markdown)には次のようなことが書いてあります。

> Standard Markdown syntax does not require a blank line before a header. Pandoc does require this (except, of course, at the beginning of the document). 

要するに見出しの前には空行が必要、ということです。

この仕様、Markdownを1行だけ出力するような場合はあまり問題にならないのですが、ループが伴うとひっかかりやすくなります。

例えば次のようなコードを実行してみましょう。

```r:input
for(i in seq_len(3)){
  cat("### lv 3 header\n")
  cat("some text")
  cat("\n")
}
```

このコードは一見問題なさそうですが、出力は次のような形となっています。

```
### lv 3 header
some text
### lv 3 header
some text
### lv 3 header
some text
```

この形は例えばQiitaならOKですが、Pandocだと（デフォルトでは）先程の仕様によって正しく処理されません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/109348/fe097873-c4c7-37e7-aa15-71b05462f103.png)

正しく処理するためには見出しの前に少なくとも1つの空行が入るように注意深く出力を組み立てる必要があります。具体的には、ループの末尾に改行を（少なくとも）2つ入れればOKです。

```r
for(i in seq_len(3)){
  cat("### lv 3 header\n")
  cat("some text")
  cat("\n\n") # 改行を少なくとも2つ入れる
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/109348/95b7f3b0-c456-d49d-5935-268ba14d32ea.png)

### `glue::glue()`を使うときの注意

`glue`パッケージの`glue()`関数はプレースホルダが利用可能で、変数を活用してテキストを組み立てるのに便利な関数です（[tidyverse/glue: Glue strings to data in R. Small, fast, dependency free interpreted string literals.](https://github.com/tidyverse/glue)）。

ですが、この関数はデフォルトで結果が`trim()`で処理されます。ヘルプには次のような記述があり、仕様であることが分かります。

> Leading whitespace and blank lines from the first and last lines are automatically trimmed.

なので、改行を入れたつもりで入ってない、というミスが起こりがちです。`glue()`の外で改行を入れるか、`.trim = FALSE`を指定することで回避できます（実は改行を2つにするという方法でも回避できますが、意図した挙動かどうかちょっと分からないのでおすすめできません）。

```r:改行はtrimされる
text <- c("foo", "bar")
cat(glue::glue("{text[1]}\n"))
cat(glue::glue("{text[2]}"))
```

```:output
foobar
```

```r:catの中で使えばOK
cat(glue::glue("{text[1]}"), "\n")
cat(glue::glue("{text[2]}"))
```

```:output
foo
bar
```

```r:.trim=FALSEを指定でもOK
cat(glue::glue("{text[1]}\n", .trim = FALSE))
cat(glue::glue("{text[2]}"))
```

```:output
foo
bar
```

### `ggplot2`を使う場合の注意

`flexdashboard`パッケージ（[flexdashboard: Easy interactive dashboards for R](https://rmarkdown.rstudio.com/flexdashboard/index.html)）とループによるMarkdown生成を組み合わせると、こんな感じでTabsetsやStoryboardを利用して、条件を少しずつ変えたプロットを比較しやすい感じで簡単に出力できるので便利です。

![output.gif](https://qiita-image-store.s3.amazonaws.com/0/109348/415c824c-1407-0a89-f628-2dc606b3abd6.gif)


Rの標準のプロットである`graphics::plot()`を使う場合は特に注意点は無いのですが、`ggplot2`だとループ時に割と問題が発生します。

`flexdashboard`のTabsetを使う例として、次のような.Rmdをknitしてみるとします。

    ---
    output:
      flexdashboard::flex_dashboard
    ---

    ```{r setup, include=FALSE}
    library(flexdashboard)
    library(dplyr)
    library(ggplot2)
    ```

    ## ggplot2 {.tabset}
    
    ```{r results='asis', echo=FALSE}
    target <- iris$Species %>% as.character %>% unique
    for(i in seq_along(target)){
      cat(glue::glue("### {i}. {target[i]}"), "\n") 
      iris %>% filter(Species == target[i]) %>% 
        ggplot(aes(Sepal.Width, Sepal.Length)) + 
        geom_point()
      cat("\n\n")
    }
    ```

しかしこれはダメです。Tabsetにはなりますが、プロットが出力されません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/109348/2b62e687-8a2b-2dc0-f831-b852f3cf7931.png)

ループ中でggplotを出力するには、一旦変数に格納してから`print()`してやります。つまり、次のようにします。

    ## ggplot2 {.tabset}
    
    ```{r results='asis', echo=FALSE}
    target <- iris$Species %>% as.character %>% unique
    for(i in seq_along(target)){
      print(glue::glue("### {i}. {target[i]}")) 
      iris %>% filter(Species == target[i]) %>% 
        ggplot(aes(Sepal.Width, Sepal.Length)) + 
        geom_point() -> p # 一旦変数に保存する必要がある
      print(p)
      cat("\n\n")
    }
    ```

これで先程示した出力が得られます。

### 参考

- [r - generate markdown comments within for loop - Stack Overflow](https://stackoverflow.com/questions/17178831/generate-markdown-comments-within-for-loop)
- [Loop in R markdown - Stack Overflow](https://stackoverflow.com/questions/33269675/loop-in-r-markdown)
- [r - plot with ggplot in for-loop doesn't work - Stack Overflow](https://stackoverflow.com/questions/21264324/plot-with-ggplot-in-for-loop-doesnt-work/21264905#21264905)