はじめに
私は、SeaPig という Markdown を HTML に変換するアプリケーションを開発しています。その中でフローチャートやシーケンス図、ガントチャートなどを Markdown 風な記法で表現し、SVG に変換できる mermaid という JavaScript のライブラリを利用しています。
上記のように書くと以下のような画像を生成してくれます。
この記事を書くきっかけ
ある時、HTML プレビューが更新される度に body の最初に空の <div> 要素が増えていくバグに気がつきました。原因がしばらく分からなかったのですが、D3.js - Data-Driven Documents を学習する機会もあり、mermaid のソースを読むことで原因が分かったのでこの記事を書くことにしました。
結論的には、ドキュメントを正確に嫁、って言われてしまいそうですけど
mermaidAPI.render() の動作について
まず、mermaidAPI.render()
のプロトタイプは、以下の通りです。
mermaidAPI.render(id, txt, cb, container)
引数 | 指定内容 |
---|---|
id | 描画したい SVG 画像の id を文字列で指定します。 |
txt | 元になる memaid 記法の文字列を指定します。 |
cb | SVG 画像の描画が完了した時に呼び出されるコールバック関数を指定します。 |
container | 作業用に使用する DOM 要素を指定します。 |
第3引数 cb と 第4引数の container を省略しても見かけ上、動作するのですが、先ほど書いたようにごみ要素が増殖してしまいます。
よく読めば、container
の説明に以下のように書いてあるので当然といえば、当然なのですが…
In one is provided a hidden div will be inserted in the body of the page instead. The element will be removed when rendering is completed.
内部的に何をやっているか
ソースを見てみます。
container
が指定された場合
以下、mermaid/src/mermaidAPI.js の 420行目 から抜粋
const render = function(id, txt, cb, container) {
if (typeof container !== 'undefined') {
container.innerHTML = '';
d3.select(container)
.append('div')
.attr('id', 'd' + id)
.append('svg')
.attr('id', id)
.attr('width', '100%')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.append('g');
} else {
関数の冒頭は、引数 container が未定義(undefined
)でない場合つまり指定された場合の処理となります。
この場合、最初に container.innerHTML = '';
を行っています。ドキュメントの container の説明には、
selector to element in which a div with the graph temporarily will be inserted.
と書いてあるので div#mermaidContainer
のようなセレクターを指定するのかと思いましたが、セレクターを指定してしまうと container.innerHTML = '';
の処理で例外が発生してしまいます。なので document.getElementById()
などで取得した Element オブジェクト指定する必要があります。
コンテナ要素を空にした後は、概ね、以下のことをやっています。
-
container
の子として、div 要素を追加 - その div 要素に
id
の先頭に 'd' という文字列を付加した id を設定 - 上記の div 要素の子として svg 要素を追加し、id や width などの属性を設定
- svg 要素に最初の g 要素(グループ化要素)を追加
`container が未定義の場合
const render = function(id, txt, cb, container) {
if (typeof container !== 'undefined') {
// (中略)
} else {
const element = document.querySelector('#' + 'd' + id);
if (element) {
element.innerHTML = '';
}
d3.select('body')
.append('div')
.attr('id', 'd' + id)
.append('svg')
.attr('id', id)
.attr('width', '100%')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.append('g');
}
- 指定された
id
の先頭に 'd' という文字列を付与した要素がないかを検索し、内容を空にします。 - body 要素の子として div 要素を追加し、
id
の先頭に 'd' という文字列を付与した id を設定(既存のものがあれば再利用) - 上記の div 要素の子として svg 要素を追加し、id や width などの属性を設定
- svg 要素に最初の g 要素(グループ化要素)を追加
というわけで
mermaidAPI.render() を使用する場合は、フロントエンドとなる HTML 内に作業用の要素をあらかじめ用意しておいて、第4引数に与えてやった方が良いです。
d3 を使っている関係上、DocumentFragment 内の要素を与えることは出来ません。
この際、コールバック関数を使わない場合は、第3引数に undefined
を与えれば良いです。null
だと例外が発生するので注意しましょう。
ちなみにコールバック関数を使用する部分のソースが、mermaid/src/mermaidAPI.js の 561行目 にあります。
if (typeof cb !== 'undefined') {
switch (graphType) {
case 'flowchart':
cb(svgCode, flowDb.bindFunctions);
break;
case 'gantt':
cb(svgCode, ganttDb.bindFunctions);
break;
default:
cb(svgCode);
}
} else {
logger.debug('CB = undefined!');
}
最後に使用例を。
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>mermaidAPI.render() 実行例</title>
<script src="mermaid.min.js"></script>
<style>
textarea#svgCode {
width: 90%;
height: 50vh
}
</style>
</head>
<body>
<div id="mermaidContainer"></div>
<textarea id="svgCode"></textarea>
<div id="svgContainer"></div>
<script>
const mermaidAPI = mermaid.mermaidAPI;
const diagram = `sequenceDiagram
participant main
participant renderer
participant previewer
main->>renderer: Open file
renderer->>previewer: Refresh preview`;
const svgId = "mermaidExample";
const container = document.getElementById("mermaidContainer");
const svgCode = document.getElementById("svgCode");
const svgContainer = document.getElementById("svgContainer");
// mermaidAPI をまず初期化します
mermaidAPI.initialize({
startOnLoad: false,
theme: null
});
// mermaid 記法から SVG を作成し、シリアライズされた文字列を取得します。
var svg = mermaidAPI.render(svgId, diagram, undefined, container);
svgCode.innerText = svg;
svgContainer.insertAdjacentHTML('afterbegin', svg);
</script>
</body>
</html>
Internet Explorer では動作しないのであしからず…。
2019/10/13追記 display:none の要素を container にした場合に必要な設定について
ガントチャートや円グラフなどで幅が取得できず、描画が崩れるため、mermaidAPI 初期化時に以下の設定を行う必要があります。
mermaidAPI.initialize({
// ここにその他、必要な設定を書く。
gantt: {
useWidth: 800
},
class: {
useWidth: 800
}
});
2019/10/14追加 結論として display:none な要素を container に指定すると描画が崩れます
隠したければ、position:absolute や z-index を低くして裏に隠してしまうというのが、良いようです。