はしくれエンジニアもどきのメモ

情報系技術・哲学・デザインなどの勉強メモ・備忘録です。

Jupyter上にGraphvizの図をSVGで描画する

Jupyter上にGraphvizの図をSVGで描画する

Jupyter上にGraphviz(.dot)の図をSVGで描画することに成功したのでメモとして残す。(※拡張機能としてはまだ作れていない)

Viz.jsのおかげでJavaのインストールすら不要になった..!

dotファイルのコードを書いてあげればこうなる👇

f:id:cartman0:20220211190623p:plain
jupyter上にGraphvizの図を描画

gist上では表示されないがnbviewerを通せば図も表示される。

https://nbviewer.org/gist/Cartman0/5a7cfadc2f367869f2df506577ace525

また、IPythonのカーネル周りの挙動もドキュメントがなく分かりづらいのメモとして残す。

試した環境

DockerDesktopはインストール済みを想定

  • Windows10 64bit
    • DockerDesktop 4.4
      • scipy-notebook Image

how to use

gist:

gist.github.com

  1. 最初のセルのコードをノートブックの一番上などで事前に実行しておく(CDNのjsファイルを読み込むので最初はオンライン環境でないといけない)
  2. drawDot関数にdotファイル中身の文字列を渡して実行

動作の仕組み

Viz.jsやIPython.notebook.kernelの仕様がわかりにくいのでメモ。 IPython.notebook.kernelに至っては詳しいドキュメントがなくjupyterのショートカットのコードあたりを読まないと挙動がわからん感じになってる。

今回はほぼJavaScriptで動作しており出力セルへの表示だけpythonが行っている。

  1. RequireJSでViz.jsとそれ用のrender.jsを読み込む(Jupyterは外部スクリプトscriptタグだけで読み込めないらしいので)
  2. Vizインスタンスの作成(Graphvizのdotコードを画像に変換してくれる)
  3. Viz.renderSVGElement()関数で.dotコードをsvgに変換
  4. このsvgはjsのSVGElementオブジェクトなのでこれをpythonに渡せるようにシリアライズして文字列(改行あり)に変換
  5. IPython.notebook.kernel.excuteにpyrhon側のSVG関数を渡して出力セルに表示させる。cell.output_area.append_output({})関数を使って出力セルに表示させるとpythonコードを実行したときのようにノートブックファイル(.ipynb)にsvgコードが保存される。

Viz.jsとは

GraphvizJavascriptEcmaScript)で書いたもの。NodeJS用かと思ったらCDNがありブラウザ上でも動作する模様。

ブラウザ上で動かしている例:<初心者>Vue.jsとviz.jsでgraphvizのオンラインエディタを作ってみた。<Vue.js練習> - Qiita

恐らく2系より仕様が変わりrenderと分かれるようになり、dotコードを変換するにはVizインスタンスを作成してそこから関数を呼ぶ必要がある。

iPythonとJavaScriptの連携

この辺、情報が少ないのでメモ

iPython上でJSを動かす方法

まず iPython上でもJSは動く。 主に2種類ある。

  • iPython.display.HTMLかマジックコマンド%%HTMLを使って<script>タグ内にJSコードを書く
    %%HTML
    <script>
      ..// your js code
      </script>
    
  • iPython.display.Javascriptかマジックコマンド%%javascriptを使う
    from iPython.display import Javascript
    Javascript('''
      ..// your js code
    ''')
    

この2つに大きな違いはないが、thisの参照先が変わるので注意。

iPython上のJSからpythonの実行

iPython上のJSのメリットとしてJSからpythonを実行できる。 これにより、

  • JSの計算結果をpythonに渡して処理(今回はこっち側に近い)
  • pythonの計算結果をコールバックで受け取り、JSで表示

などが可能になる。

例:numpyの計算結果をJSのalertで表示している:

enakai00.hatenablog.com

JS側で使えるAPIとしてiPython.notebook.kernelがある。 このiPython.notebook.kernel.execute()関数にpythonコードの文字列を渡すとバックグラウンド上で実行される。

サンプルコード例1:

from IPython.display import Javascript

jscode = '''
var kernerl=IPython.notebook.kernel;
var jsobj = "hello world";
var pycode = `"""${jsobj}""`;
kernel.execute(pycode);
'''
Javascript(jscode)

注意1:python側で文字列リテラルとして認識させるためにダブルクオーテーションやシングルクォーテーション等つけてサニタイズする必要がある。

JS側でpythonの計算結果を受け取るにはコールバックを渡す. ブラウザ上のコンソールに表示するだけならコールバックの結果を表示すればいい。

サンプルコード例2:

from IPython.display import Javascript

jscode = '''
var kernel = IPython.notebook.kernel;

var callback = function(output) {
    console.log(output);
    var res = output.content.data['text/plain'];
    console.log("o:", res);
    return res;
};

var x = 0;
var pycode = `"""hello world """ + str(${x}+1)`;
kernel.execute(pycode,  {'iopub': {"output": callback}},  {'silent':false});
'''

Javascript(jscode)

ブラウザからコンソールを開く。ちなみにpython側でエラーが出るとコールバック引数outputにTracebackが保存される。

コンソール :

{header: {…}, msg_id: '66a569b4-e88df9f760b38eff2762703f_219', msg_type: 'execute_result', parent_header: {…}, metadata: {…}, …}
o: 'hello world 1'

上記の例だと、セルのOutには何も結果が表示されない。 表示させるには、セルオブジェクトを取得してcell.output_area.append_output()関数が使える。

サンプル3:

from IPython.display import Javascript

jscode = '''
var kernel = IPython.notebook.kernel;

function get_exec_cell(this_of_call){
    var output_area = this_of_call;
    var cell_element = output_area.element.parents('.cell');
    var cell_idx = Jupyter.notebook.get_cell_elements().index(cell_element);
    var cell = Jupyter.notebook.get_cell(cell_idx);
    return cell;
}

var callback = function(output) {
    console.log(output)
    var res = output.content.data['text/plain'];
    console.log("o:", res);
    var cell = get_exec_cell($this);
    cell.output_area.clear_output();
    cell.output_area.append_output({
        output_type: "stream",
        name: "o",
        text: String(res)
    });
    return res;
};

var x = 0;
var pycode = `"""hello world """+str(${x}+1)`;
$this=this
kernel.execute(pycode,  {'iopub': {"output": callback}},  {'silent':false});
'''

Javascript(jscode)

関数に渡すにはオブジェクトにする必要がある。必要なプロパティが決まっている。プロパティを満たさないとnotebookファイルのjsonがvalidにならない。

まず output_typeを指定する必要がある。

テキスト出力の場合: output_type: "stream"name:(名前)、text: 出力したい文字列、の3つが必要

//テキストの出力
cell.output_area.append_output({
      output_type: "stream",
      name: "o",
      text: String(res)
});

画像出力の場合:output_type: "display_data"dataの2つが必要。 dataMIMEタイプを指定して画像データを渡す。

//画像の出力
cell.output_area.append_output({
  output_type: "display_data",
  data: {"image/svg+xml": out.content.data["image/svg+xml"]}
});

※注意:実行中のセルの取得にJupyter.notebook.get_selected_cell();は使えないので注意。SHIFT+ENTERでセル実行後の次のセルが選択されてしまう。参考: ipython - How to select current cell with JavaScript in Jupyter? - Stack Overflow