Rmdスライドにグリッドレイアウトを導入（revealjs＋Pure.css）

この記事は、[RStudio Advent Calendar 2017 - Qiita](https://qiita.com/advent-calendar/2017/rstudio) 11日目の記事です。
RStudioそのものではなく、Rmdによるスライド作成の話題で恐縮ですが投稿させて頂きます。

# 1. はじめに

RStudio/RMarkdownを用いることで、発表用のスライドも作成することができます。
私は、@kazutanさんによる [R Markdownによるスライド生成](https://kazutan.github.io/SappoRoR6/rmd_slide.html) を拝見して以来、いくつかあるRmdスライドの中で `revealjs` パッケージを使ったhtmlスライドを愛用しています。
`revealjs` によるスライドの不満として、2カラムレイアウトが（html直書きをしない限り）できない点がありました。htmlスライドで複雑なレイアウトは適当でないかもしれませんが、グラフの補足など横に添えられると良いなという場面があるため、対処を考えた記録です。

以下のようなスライドを実現することを目標とします：

![test-5.png](https://qiita-image-store.s3.amazonaws.com/0/137711/acb68dbf-b6c9-4bfb-0a17-603f15520f1a.png)


## 1-1. 検証環境

色々と試行錯誤できるように、Dockerイメージ `rocker/tidyverse:3.4.2` を `tokyor/rstudio` に倣って日本語化したものを準備しました。そのままでは肝心の `revealjs` パッケージがインストールされていないのでインストールしておきます。

```r
install.packages("revealjs", dependencies = TRUE)
```

各々のバージョン情報は以下の通りです。

```r
> devtools::session_info()$platform
 setting  value                       
 version  R version 3.4.2 (2017-09-28)
 system   x86_64, linux-gnu           
 ui       RStudio (1.1.383)           
 language (EN)                        
 collate  ja_JP.UTF-8                 
 tz       Japan                       
 date     2017-12-11                  

> devtools::session_info(pkgs = c("revealjs", "tidyverse"))$packages %>% filter(`*` == "*")
     package * version       date         source
1   bindrcpp *     0.2 2017-06-17 CRAN (R 3.4.2)
2      dplyr *   0.7.4 2017-09-28 CRAN (R 3.4.2)
3    forcats *   0.2.0 2017-01-23 CRAN (R 3.4.2)
4    ggplot2 *   2.2.1 2016-12-30 CRAN (R 3.4.2)
5   graphics *   3.4.2 2017-12-01          local
6  grDevices *   3.4.2 2017-12-01          local
7    methods *   3.4.2 2017-12-01          local
8      purrr *   0.2.4 2017-10-18 CRAN (R 3.4.2)
9      readr *   1.1.1 2017-05-16 CRAN (R 3.4.2)
10  revealjs *     0.9 2017-03-13 CRAN (R 3.4.2)
11     stats *   3.4.2 2017-12-01          local
12   stringr *   1.2.0 2017-02-18 CRAN (R 3.4.2)
13    tibble *   1.3.4 2017-08-22 CRAN (R 3.4.2)
14     tidyr *   0.7.2 2017-10-16 CRAN (R 3.4.2)
15 tidyverse *   1.2.1 2017-11-14 CRAN (R 3.4.2)
16     utils *   3.4.2 2017-12-01          local
```

ホストはFall Creators Update 適応済の Windows 10 Pro 64bit ＋ Oracle VM VirtualBox 5.1.30 です。

## 1-2. グリッドレイアウトのフレームワーク

今回は、Yahoo, Inc.により開発・提供されている軽量なCSSフレームワークである[Pure.css](https://purecss.io/) のうち`Grids`モジュールのみを使用しました。
特徴として、ほかの多くのフレームワークは横を12グリッドに分割しているのに対し、Pure.cssは5ないし24グリッドに分割しているためより柔軟な指定が可能です。

# 2. revealjsの出力を確認

`File - New File - R Markdown - From Template` と進み `Reveal.js Presentation (HTML)` を選びます。
続いて、コードを以下のように単純化したものに書き換えてKnitします。

```:test.Rmd
---
title: "Test"
output: revealjs::revealjs_presentation
---

## Test

### h3 {.two-col}

#### h4 {.each-col}

left

#### h4 {.each-col}

right
```

生成された .html のコアの部分は以下のようになっており、Pandocの `--section-div` オプションは有効になっていないようです。

```html:test.html
<section id="test" class="slide level2">
  <h2>Test</h2>
  <h3 id="h3" class="two-col">h3</h3>
  <h4 id="h4" class="each-col">h4</h4>
  <p>left</p>
  <h4 id="h4-1" class="each-col">h4</h4>
  <p>right</p>
</section>
```
yamlフロントマターを変更すると、出力は次のように変わります。

```yaml:test.Rmdフロントマター
output: 
  revealjs::revealjs_presentation:
    pandoc_args: [--section-divs]
```
```html:test.html修正後
<section id="test" class="slide level2">
  <h2>Test</h2>
  <section id="h3" class="level3 two-col">
    <h3>h3</h3>
    <section id="h4" class="level4 each-col">
      <h4>h4</h4>
      <p>left</p>
    </section>
    <section id="h4-1" class="level4 each-col">
      <h4>h4</h4>
      <p>right</p>
    </section>
  </section>
</section>
```

また `revealjs` スライドの場合は、通常の `html_document` のようにjQueryやBootstrapは組み込まれていません。

# 3. とりあえずの方法

Pure.cssのグリッドレイアウトは、各レイアウト要素の幅をクラス `.pure-u-*` で指定し、全体をクラス `.pure-g` で包むことで指定します。したがって、`--section-divs` を有効にした上で各レベルの h* 要素に `{.class}` を付ければ解決しそうです。
（###, #### を使う方法は、[R MarkdownにBootstrapのPanels機能を実装](https://qiita.com/kazutan/items/ca27e2f5ea0ba3a1dcf3) を参考にしました）

```md:test2.Rmd
---
title: "Test"
output: 
  revealjs::revealjs_presentation:
    pandoc_args: [--section-divs]
    css: https://unpkg.com/purecss@1.0.0/build/grids-min.css
---

## Test

### {.pure-g}

#### {.pure-u-1-2}

left

#### {.pure-u-1-2}

right
```

しかし、実際はこのようにうまく行きませんでした。

![test-2.png](https://qiita-image-store.s3.amazonaws.com/0/137711/85491739-1e06-5232-4c24-28a74a0e7464.png)


htmlのソースでは特に問題はないように見えますが、Chrome開発者ツールで開くと当該部分は以下のようになっています。

```html
<section id="test" class="slide level2 stack present" style="display: block;" data-previous-indexv="0">
  <h2>Test</h2>
  <section id="section" class="level3 pure-g stack present" style="display: block;">
    <h3></h3>
    <section id="section-1" class="level4 pure-u-1-2 future" aria-hidden="true" style="display: block;">
      <h4></h4>
      <p>left</p>
    </section>
    <section id="section-2" class="level4 pure-u-1-2 future" aria-hidden="true" style="display: block;">
      <h4></h4>
      <p>right</p>
    </section>
  </section>
</section>
```

Pure.css 1.0 では、グリッドレイアウトをflexboxにより実現しているため、`.pure-g` の要素は `style="display:flex;"` である必要がありますが、`display: block` になってしまっています。また、.Rmdの内容によっては `style="display:none;"` になってしまうこともあります。
それを回避するためcssチャンクを作って以下のように上書きすると、RStudio ServerのビューアーやChrome上では無事2カラムになりました。

```css:css
.reveal .pure-g {
  display: flex !important;
}

.reveal .pure-u-1-2 {
  display: block !important;
}
```

![test-4.png](https://qiita-image-store.s3.amazonaws.com/0/137711/c8f60b0e-8442-41df-15ec-fe26ac658099.png)

## 3-1. この方法の問題点

一見うまくできたように見えますが、この方法には以下のような問題があります。

1. （Windows）デスクトップ版のRStudioのビューアーではうまく表示できない
2. ブラウザで開いて、印刷したりPDF化しようとすると大幅にズレてしまう

デスクトップ版のRStudioで確認しながら作成したい、資料としてPDF化したいなどの需要がある場合は問題です。

# 4. 最終的な解決策

Windowsデスクトップ版のRStudioのビューアーにも、右クリックから起動できるInpsectorがあります。こちらで色々と触ってみたところ、`.pure-*` に関連する要素を &lt;section&gt; から &lt;div&gt; に変えれば期待通りの結果になることがわかりました。
また、`.pure-u-*` は非常に多くの組み合わせがあるため、必要なものだけでも漏れなくユーザーCSSに記載するのは大変です。
これらをまとめてJavascriptで処理することとしました。

> 【2017.12.14 訂正】
> 1) 当初のものは、印刷時のズレが解消できていませんでした。
> これはスライド内に複数の&lt;section&gt;を有する場合は当該スライド全体を囲む&lt;section&gt;に`.stack`クラスが追加され印刷時に余白の設定が変わってしまうことが原因と考え、`.stack`を消す対応としました。
>
> 2) スライド内で&lt;h3&gt;以下の見出しを使った場合、スライドの遷移などに問題がありました。
> Reveal.jsでは&lt;section&gt;がスライドの区切り（改ページ）となっているので、`--section-divs`が有効な状態でスライド内で&lt;h3&gt;以下の見出しを使うと&lt;section&gt;が複数できて改ページが生じます。その際に&lt;h3&gt;以下の見出しが非ASCIIの場合は`{#id}`を適切に振っていないと冒頭に強制的に戻されてしまいます。Pure.cssに関連しない&lt;section&gt;も操作に影響しない範囲で&lt;div&gt;に置換するようにしました。
>
> 3) 空のh*要素が残ることによる余計な余白を出さないために、空のh*要素は削除するようにしました。

```js:20171214訂正版
(function(){
  // display: none; になっているところがあるので、全て表示させる
  var gridElements = document.querySelectorAll(".slides .level1 section, .slides .level2 section");
  for(var i = 0; i < gridElements.length; i++){
    gridElements[i].style.display = "block";
  }

  // 各スライドコンテンツ内の <section> を <div> に置換
  // .stack は削除しないと印刷のときにずれてしまう
  gridElements = document.querySelectorAll(".slides .level1, .slides .level2");
  for(i = 0; i < gridElements.length; i++){
    gridElements[i].classList.remove("stack");
    gridElements[i].innerHTML = 
      gridElements[i].innerHTML.replace(/(<[/]?)section/g, "$1div");
  }
  
  // div.pure-g を display: flex; にする
  gridElements = document.querySelectorAll("div.pure-g");
  for(i = 0; i < gridElements.length; i++){
    // element.style.display では !important が上手く付加できない？
    gridElements[i].setAttribute("style", "display: flex !important;");
  }

  // .pure-g 以下に残った空の <h*> 要素を削除する
  gridElements = document.querySelectorAll(".pure-g h3, .pure-g h4, .pure-g h5, .pure-g h6");
  for(i = 0; i < gridElements.length; i++){
    if(gridElements[i].textContent === "") {
      gridElements[i].parentNode.removeChild(gridElements[i]);
    }
  }

}());
```

不勉強で理由が分かりませんが、Chromeではうまく動く forEach() を使った処理はビューアーでは動かなかったためforループで処理しています。

## 4-1. 2カラム（2:1幅）のサンプル

これらを全て取り入れたサンプルです。Javascriptは jsチャンクではなく別ファイルにして、`includes: after_body:` で文書の最後に取り込みます。

```html:for_purecss.html（20171214訂正版）
<script>
 (function(){
    // display: none; になっているところがあるので、全て表示させる
    var gridElements = document.querySelectorAll(".slides .level1 section, .slides .level2 section");
    for(var i = 0; i < gridElements.length; i++){
      gridElements[i].style.display = "block";
    }

    // 各スライドコンテンツの <section> を <div> に置換し、.stack は削除する
    gridElements = document.querySelectorAll(".slides .level1, .slides .level2");
    for(i = 0; i < gridElements.length; i++){
      gridElements[i].classList.remove("stack");
      gridElements[i].innerHTML = 
        gridElements[i].innerHTML.replace(/(<[/]?)section/g, "$1div");
    }
    
    // div.pure-g を display: flex; にする
    gridElements = document.querySelectorAll("div.pure-g");
    for(i = 0; i < gridElements.length; i++){
      gridElements[i].setAttribute("style", "display: flex !important;");
    }

    // .pure-g 以下に残った空の <h*> 要素を削除する
    gridElements = document.querySelectorAll(".pure-g h3, .pure-g h4, .pure-g h5, .pure-g h6");
    for(i = 0; i < gridElements.length; i++){
      if(gridElements[i].textContent === "") {
        gridElements[i].parentNode.removeChild(gridElements[i]);
      }
    }

  }());
</script>
```

```md:test4.Rmd
---
title: "Test4"
output: 
  revealjs::revealjs_presentation:
    pandoc_args: [--section-divs]
    css: https://unpkg.com/purecss@1.0.0/build/grids-min.css
    includes:
      after_body: for_purecss.html
---

## Iris

### {.pure-g}

#### {.pure-u-2-3}

    ```{r, echo=FALSE, warning=FALSE, message=FALSE}
    library(tidyverse)
    tbl_df(iris) %>%
      ggplot(aes(x = Sepal.Length, y = Sepal.Width, color = Species)) +
        geom_point(size = 3) +
        guides(color = FALSE)
    ```
 
#### 凡例 {.pure-u-1-3}

Red
: _Iris setosa_

Green
: _Iris vesicolor_

Blue
: _Iris virginica_
```

このページの冒頭のスクリーンショットは、Windowsデスクトップ版のRStudio 1.1.383で上記をKnitした際のスクリーンショットです。


# 5. さいごに

当初は、これでもうまく行かないためJavascriptでDOMツリーを作り直す力技で対応していました。その場合はデスクトップ版RStudioのビューアーではPure.cssの適用部分がまったく表示されず確認の役に立たないのが悩みでしたが、今回あらためてやってみたところ上記の方法でできたため、スライド作成がかなり楽になりました。
今思えば、forループの代わりにforEach()を多用するコードになっていたのが理由かもしれません。

長文失礼いたしました。ご参考になりましたら幸いです。