この記事は、RStudio Advent Calendar 2017 - Qiita 11日目の記事です。
RStudioそのものではなく、Rmdによるスライド作成の話題で恐縮ですが投稿させて頂きます。
1. はじめに
RStudio/RMarkdownを用いることで、発表用のスライドも作成することができます。
私は、@kazutanさんによる R Markdownによるスライド生成 を拝見して以来、いくつかあるRmdスライドの中で revealjs
パッケージを使ったhtmlスライドを愛用しています。
revealjs
によるスライドの不満として、2カラムレイアウトが(html直書きをしない限り)できない点がありました。htmlスライドで複雑なレイアウトは適当でないかもしれませんが、グラフの補足など横に添えられると良いなという場面があるため、対処を考えた記録です。
以下のようなスライドを実現することを目標とします:
1-1. 検証環境
色々と試行錯誤できるように、Dockerイメージ rocker/tidyverse:3.4.2
を tokyor/rstudio
に倣って日本語化したものを準備しました。そのままでは肝心の revealjs
パッケージがインストールされていないのでインストールしておきます。
install.packages("revealjs", dependencies = TRUE)
各々のバージョン情報は以下の通りです。
> 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 のうちGrids
モジュールのみを使用しました。
特徴として、ほかの多くのフレームワークは横を12グリッドに分割しているのに対し、Pure.cssは5ないし24グリッドに分割しているためより柔軟な指定が可能です。
2. revealjsの出力を確認
File - New File - R Markdown - From Template
と進み Reveal.js Presentation (HTML)
を選びます。
続いて、コードを以下のように単純化したものに書き換えてKnitします。
---
title: "Test"
output: revealjs::revealjs_presentation
---
## Test
### h3 {.two-col}
#### h4 {.each-col}
left
#### h4 {.each-col}
right
生成された .html のコアの部分は以下のようになっており、Pandocの --section-div
オプションは有効になっていないようです。
<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フロントマターを変更すると、出力は次のように変わります。
output:
revealjs::revealjs_presentation:
pandoc_args: [--section-divs]
<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機能を実装 を参考にしました)
---
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
しかし、実際はこのようにうまく行きませんでした。
htmlのソースでは特に問題はないように見えますが、Chrome開発者ツールで開くと当該部分は以下のようになっています。
<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カラムになりました。
.reveal .pure-g {
display: flex !important;
}
.reveal .pure-u-1-2 {
display: block !important;
}
3-1. この方法の問題点
一見うまくできたように見えますが、この方法には以下のような問題があります。
- (Windows)デスクトップ版のRStudioのビューアーではうまく表示できない
- ブラウザで開いて、印刷したりPDF化しようとすると大幅にズレてしまう
デスクトップ版のRStudioで確認しながら作成したい、資料としてPDF化したいなどの需要がある場合は問題です。
4. 最終的な解決策
Windowsデスクトップ版のRStudioのビューアーにも、右クリックから起動できるInpsectorがあります。こちらで色々と触ってみたところ、.pure-*
に関連する要素を <section> から <div> に変えれば期待通りの結果になることがわかりました。
また、.pure-u-*
は非常に多くの組み合わせがあるため、必要なものだけでも漏れなくユーザーCSSに記載するのは大変です。
これらをまとめてJavascriptで処理することとしました。
【2017.12.14 訂正】
当初のものは、印刷時のズレが解消できていませんでした。
これはスライド内に複数の<section>を有する場合は当該スライド全体を囲む<section>に.stack
クラスが追加され印刷時に余白の設定が変わってしまうことが原因と考え、.stack
を消す対応としました。スライド内で<h3>以下の見出しを使った場合、スライドの遷移などに問題がありました。
Reveal.jsでは<section>がスライドの区切り(改ページ)となっているので、--section-divs
が有効な状態でスライド内で<h3>以下の見出しを使うと<section>が複数できて改ページが生じます。その際に<h3>以下の見出しが非ASCIIの場合は{#id}
を適切に振っていないと冒頭に強制的に戻されてしまいます。Pure.cssに関連しない<section>も操作に影響しない範囲で<div>に置換するようにしました。空のh要素が残ることによる余計な余白を出さないために、空のh要素は削除するようにしました。
(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:
で文書の最後に取り込みます。
<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>
---
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()を多用するコードになっていたのが理由かもしれません。
長文失礼いたしました。ご参考になりましたら幸いです。