餡子付゛録゛

ソフトウェア開発ツールの便利な使い方を紹介。

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 MarkdownYAMLヘッダーで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を追加しています。

3.3. iframeを書き込む

後はR Markdown

<iframe style="width:100%; height:70vh; transform:scale(1.732051); transform-origin:0 0;" src="mermaid.html"></iframe>

とiframeで作成したファイルを呼び出すように書いておけば、表示を一体化できます。

なお、iframeの内側は外側よりキャッシュが強くリロードしても表示が変わらないときがあるので、作業中に困ったら、CTRL+Rを押すか、ブラウザーを一旦終了してファイルを読み直しましょう。

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)。