やりたいこと
木構造データをJupyter Notebookとか、ウェブアプリとかで美しく描画したい!というのがモチベーションです。Pythonを使うのは元データを分析する都合からで、インタラクティブに分析と可視化を繰り返したいと考えたからです。
Python
Pythonで木構造を描画するやり方はいろいろあるようでして、一番最初に思いつくのは、networkxやgraphvizとかのグラフライブラリを使うことだけど、そもそもがグラフ構造の分析を行うことが目的のライブラリなので、凝った表現は難しいと考えます。
一方で、木構造の専用の分析・可視化ツールとしては、ETE Toolkitがあります。木構造の表現としてはよく知られているらしいThe Newick formatを使ってモデルを表現します。これもなかなか良さそうですが、ウェブでの親和性を考慮すると、モデルの記述にはJSONなどのもう少し汎用性の高いものを使いたいなと考えました。
Javascript
Javascriptでのグラフ構造の描画については、ここに良いまとめがあります。いろいろありますが、汎用性や情報量の多さで言うとやはり、D3.jsかなと思います。木構造(tree)の描画についてもライブラリ化されています。
というわけで、Jupyter Notebookを使ったD3.jsでの木構造の描画を試みます。Jupyter Notebookというのは、セルマジック(%%javascript)を使うことで、Pythonカーネルを使ったままで、Javascriptの実行やHTMLの描画をすることができるので、もともと親和性は良いのです。
Jupyter NotebookでのD3.jsの利用
やり方としては、Javascriptのセルマジックを使ってやるのと、py_d3を使ってやるのが考えられます。前者はまぁ、RequireJSとかを使って、直感的には分かりにくい記述になります(ここを参照)。後者は、Javascriptのセルマジックをいい感じにラップしているようです。ここではpy_d3を使った描画の解説をします。
準備
何はともあれ、py_d3のインストールをします。pipを使いましょう。pip install py_d3
最初に、以下のようにモジュールを読み込みます。
import py_d3
py_d3.load_ipython_extension(get_ipython())
以下はホームページのサンプルの引用です。最初の%%d3 5.12.0
でd3.jsのバージョン指定をしています。どのバージョンが使えるかは、%d3 version
とコマンドを打てば分かります。
%%d3 5.12.0
<g></g>
<style>
element {
height: 25px;
}
div.bar {
display: inline-block;
width: 20px;
height: 75px;
margin-right: 2px;
background-color: teal;
}
</style>
<script>
var dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];
d3.select("g").selectAll("div")
.data(dataset)
.enter()
.append("div")
.attr("class", "bar")
.style("height", function(d) {
var barHeight = d * 5;
return barHeight + "px";
});
</script>
Pythonとのデータ連携
RequireJSを駆使すれば、定義したJavascriptの関数に、Pythonで計算したオブジェクトや変数を入力することは可能です。ただ、JSONで出力してd3.jsで読み込むのと大差ないので、あまり巨大なデータでなければ、中間ファイルを経由するのが簡単です。例えば、以下のように木構造のJSONファイルを準備します。
import json
data = {
"name": "A",
"children": [
{ "name": "B" },
{
"name": "C",
"children": [{ "name": "D" }, { "name": "E" }, { "name": "F" }]
},
{ "name": "G" },
{
"name": "H",
"children": [{ "name": "I" }, { "name": "J" }]
},
{ "name": "K" }
]
};
json_file = open('test.json', 'w')
json.dump(data, json_file)
木構造の描画
木構造の描画には、d3 hierarchyのツリーレイアウトを使います。JSONデータ構造をロードして、そのデータをもとにtreeを作ったら、あとはSVGの描画の調整をするだけです。描画の細かい調整については、d3.jsやSVGのドキュメントを参照ください。
%%d3 5.12.0
<style>
.link {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
stroke-width: 1.5px;
}
</style>
<svg width="800" height="600"></svg>
<script>
var width = 800;
var height = 600;
var g = d3.select("svg").append("g")
.attr("transform", "translate(80,0)");
console.log("data");
d3.json("test.json")
.then((data) => {
console.log(data);
var root = d3.hierarchy(data);
var tree = d3.tree(root).size([height, width - 160]);
tree(root);
var link = g.selectAll(".link")
.data(root.descendants().slice(1))
.enter()
.append("path")
.attr("class", "link")
.attr("d", (d) => {
return "M" + d.y + "," + d.x +
"C" + (d.parent.y + 100) + "," + d.x +
" " + (d.parent.y + 100) + "," + d.parent.x +
" " + d.parent.y + "," + d.parent.x;
});
var node = g.selectAll(".node")
.data(root.descendants())
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
node.append("circle")
.attr("r", 8)
.attr("fill", "#999");
node.append("text")
.attr("dy", 3)
.attr("x", function(d) { return d.children ? -12 : 12; })
.style("text-anchor", function(d) { return d.children ? "end" : "start"; })
.attr("font-size", "200%")
.text(function(d) { return d.data.name; });
})
.catch((error)=>{
}
)
</script>