LoginSignup
4
1

More than 3 years have passed since last update.

mermaidAPI.render() を呼び出すときは container も指定しましょうというお話

Last updated at Posted at 2019-10-05

はじめに

私は、SeaPig という Markdown を HTML に変換するアプリケーションを開発しています。その中でフローチャートやシーケンス図、ガントチャートなどを Markdown 風な記法で表現し、SVG に変換できる mermaid という JavaScript のライブラリを利用しています。

sequenceDiagram
    participant main
    participant renderer
    participant previewer
    main->>renderer: Open file
    renderer->>previewer: Refresh preview

上記のように書くと以下のような画像を生成してくれます。

mermaid.png

この記事を書くきっかけ

ある時、HTML プレビューが更新される度に body の最初に空の <div> 要素が増えていくバグに気がつきました。原因がしばらく分からなかったのですが、D3.js - Data-Driven Documents を学習する機会もあり、mermaid のソースを読むことで原因が分かったのでこの記事を書くことにしました。

結論的には、ドキュメントを正確に嫁、って言われてしまいそうですけど :sweat_smile:

mermaidAPI.render() の動作について

まず、mermaidAPI.render() のプロトタイプは、以下の通りです。

mermaidAPI.render(id, txt, cb, container)
引数 指定内容
id 描画したい SVG 画像の id を文字列で指定します。
txt 元になる memaid 記法の文字列を指定します。
cb SVG 画像の描画が完了した時に呼び出されるコールバック関数を指定します。
container 作業用に使用する DOM 要素を指定します。

第3引数 cb と 第4引数の container を省略しても見かけ上、動作するのですが、先ほど書いたようにごみ要素が増殖してしまいます。

よく読めば、container の説明に以下のように書いてあるので当然といえば、当然なのですが… :sweat_smile:

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行目 から抜粋

mermaid/src/mermaidAPI.js(抜粋)
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 が未定義の場合

mermaid/src/mermaidAPI.js(抜粋)
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引数に与えてやった方が良いです。

:exclamation: d3 を使っている関係上、DocumentFragment 内の要素を与えることは出来ません。

この際、コールバック関数を使わない場合は、第3引数に undefined を与えれば良いです。null だと例外が発生するので注意しましょう。

ちなみにコールバック関数を使用する部分のソースが、mermaid/src/mermaidAPI.js の 561行目 にあります。

mermaid/src/mermaidAPI.js(抜粋)
  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!');
  }

最後に使用例を。

sample.html
<!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 を低くして裏に隠してしまうというのが、良いようです。

参考リンク

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1