DiagrammeR/Mermaid.jsで描いた図をreveal.jsに何とか埋め込む方法
Rで簡単にフローチャートやシーケンス図やER図を描いてくれるDiagrammeRパッケージで生成した図を、標準的な方法でR Markdown Format for reveal.js Presentationsで使おうとしたら、微妙に挙動不審なhtmlが出来上がったので、問題の回避策を記しておきたいとおもいます。
1. 標準的な方法とその問題
.Rmdのマークダウン記法の部分のRのコードチャンクに
DiagrammeR::mermaid("graph LR; d3.js-->Mermaid.js; d3.js-->Graphviz; Mermaid.js-->DiagrammeR; Graphviz-->DiagrammeR;")
と描くのが最も簡潔な方法ですが、適切に表示されたりされなかったり*1、他のスライドのCSSの設定が変化してしまったり、挙動不審になります。
解決方法がないか検索してみたのですが、どうもreveal.jsとMermaid.jsがその構造から相性が悪いようなので諦める事にします。
2. SVGに吐き出す方法とその問題
DiagrammeR::mermaidは、htmlwidgetオブジェクトを生成します。DiagrammeR::grVizもhtmlwidgetオブジェクトを生成し、そのhtmlwidgetオブジェクトはSVGに変換して保存することができます*2。ならば、同様に、DiagrammeR::mermaidが生成したhtmlwidgetオブジェクトもSVGに変換したいところですが、Rが異常終了するので無理そうでした。
library(DiagrammeR) library(DiagrammeRsvg) m <- mermaid("graph LR; d3.js-->Mermaid.js; d3.js-->Graphviz; Mermaid.js-->DiagrammeR; Graphviz-->DiagrammeR;") svg <- export_svg(m) # Rごと異常終了する con <- file("m.svg", "w") writeLines(m, con) close(con)
html/javascriptの生成物がSVGに変換できるとは限らないわけで、DiagrammeRsvgのアップデートで改善される見込みも薄そうです。
3. htmlを生成してiframeで表示する
こういうわけで、htmlを生成してiframeで表示する方法を試します。
3.1. self_contained: false
R MarkdownのYAMLヘッダーでself_contained: falseを指定します。
output: revealjs::revealjs_presentation: self_contained: false
プレゼン資料がファイル一枚でまとまらず、サブディレクトリも持っていく必要があるのが残念なところですが、どうもreveal.jsはiframeの扱いが上手くないので。
3.2. htmlの生成と保存
R Markdownのコードチャンクの中で
library(DiagrammeR) library(htmlwidgets) saveWidget <- function(...){ args <- list(...) w <- args$widget sp <- w$sizingPolicy # 今後のhtmlwidgetsのアップデートでプロパティ名は変化する可能性あり sp$padding <- 0 if(is.null(args[["width"]])) args$width <- 400 if(is.null(args[["height"]])) args$height <- 300 sp$defaultWidth <- args$width sp$defaultHeight <- args$height args["width"] <- args["height"] <- NULL w$sizingPolicy <- sp args$widget <- w do.call(htmlwidgets::saveWidget, args) con <- file(args[["file"]], "r", blocking=FALSE) lines <- readLines(con, encoding="UTF-8") targets <- grep("</body[^>]*>", lines) if(0<length(targets)){ lines[targets[1]] = gsub("(</body[^>]*>)", "<script>if(0>=document.body.clientWidth){ setTimeout(function(){ location.reload(); }, 1000) }</script>\\1", lines[targets[1]]) } close(con) con <- file(args[["file"]], "w", encoding="utf-8") writeLines(lines, con) close(con) } g <- mermaid("graph LR; d3.js-->mermaid; d3.js-->Graphviz; mermaid-->DiagrammeR; Graphviz-->DiagrammeR;") saveWidget(widget=g, file="mermaid.html", background="transparent", selfcontained=FALSE, title = "example", width = 400, height = 200) # defaultWidthとdefaultHeightの値は図のサイズに応じて変える
とhtmlを生成して保存します。
saveWidgetにフックして、パラメーターをすりかえつつ、生成したファイルにJavaScriptを追加しているわけですが、JavaScriptの追加理由が分かりづらいかも知れません。reveal.jsの中ではiframeのサイズが動的に変化するのですが、Mermaid.jsは読み込み時に1回しか描画しないため、サイズ0で描画して終わりになる場合があります。そこでiframeのサイズが0の間は、1秒間隔でiframeをreloadするJavaScriptを追加しています。
4. まとめ
気づくとjQueryやVue.jsに限らず無数にあるJavaScriptのフレームワークですが、諸般の理由で同時利用が困難なことがあります*3。何とか両方使いたいときは、iframeで片方を隔離してしまうのが無難です。
*1:Mermaid.jsの図があるページでリロードしないと表示しないように思えました。以下のようになります。
*2: DiagrammeR入門 画像ファイルなどへの出力 - Qiita
*3:R Markdownの発展版であるQuartoでも、コードチャンクの実行後にMermaidで描いた図が表示されないと言う問題が2022年9月28日時点である(Quarto Revealjs - Mermaid diagrams don't show after R code chunks, pictures - R Markdown - RStudio Community)。