JavaScript
SVG
d3.js
健康

d3.jsでスゴイっぽい図(force layout)を作ってみたら思ったより簡単だった件

スゴイっぽい図((force layout))って何

スクリーンショット 2016-10-23 17.58.33.png

こんな感じの、なんかビヨーンとなってシャキシャキして動くやつ。

説明するより動くものを見てもらった方が早いかも。

作ったdemo

やってみた感想

  • グワングワン動いて楽しい
  • 動的な描画をするのは難しそうだと思ってたが、そうでもなかった。
  • d3.js自体も難しいものだと避けてきたが、たくさんの人が記事にあげてくれているからググればなんとかなる。
  • 実際に何かに使うには、描画したいデータを必要なフォーマットに直すところがちょっと大変そう。

解説

0. 全ソース

一旦全ソースを貼ってみる。

コピペしていくつかの値をいじってみるだけでも楽しい。

d3_test.html
<!DOCTYPE html>
<html>
<head>
    <title>d3test</title>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<style type="text/css">

</style>
<script type="text/javascript">
    var width = 800;
    var height = 800;
    // nodeの定義。ここを増やすと楽しい。
    var nodes = [
        {id:0, label:"nodeA"},
        {id:1, label:"nodeB"},
        {id:2, label:"nodeC"},
        {id:3, label:"nodeD"},
        {id:4, label:"nodeE"},
        {id:5, label:"nodeF"},

    ];

    // node同士の紐付け設定。実用の際は、ここをどう作るかが難しいのかも。
    var links = [
        {source:0, target:1},
        {source:0, target:2},
        {source:1, target:3},
        {source:1, target:3},
        {source:2, target:1},
        {source:2, target:3},
        {source:3, target:4},
        {source:4, target:5},
        {source:5, target:3}
    ];
    // forceLayout自体の設定はここ。ここをいじると楽しい。
    var force = d3.layout.force()
        .nodes(nodes)
        .links(links)
        .size([width, height])
        .distance(140) // node同士の距離
        .friction(0.9) // 摩擦力(加速度)的なものらしい。
        .charge(-100) // 寄っていこうとする力。推進力(反発力)というらしい。
        .gravity(0.1) // 画面の中央に引っ張る力。引力。
        .start();

    // svg領域の作成
    var svg = d3.select("body")
        .append("svg")
        .attr({width:width, height:height});

    // link線の描画(svgのline描画機能を利用)
    var link = svg.selectAll("line")
        .data(links)
        .enter()
        .append("line")
        .style({stroke: "#ccc",
        "stroke-width": 1
    });

    // nodesの描画(今回はsvgの円描画機能を利用)
    var node = svg.selectAll("circle")
        .data(nodes)
        .enter()
        .append("circle")
        .attr({
            // せっかくなので半径をランダムに
            r: function() {return Math.random() * (40 - 10) + 10;}
        })
        .style({
            fill: "orange"
        })
        .call(force.drag);

    // nodeのラベル周りの設定
    var label = svg.selectAll('text')
        .data(nodes)
        .enter()
        .append('text')
        .attr({
            "text-anchor":"middle",
            "fill":"white",
            "font-size": "9px"
        })
        .text(function(data) { return data.label; });

    // tickイベント(力学計算が起こるたびに呼ばれるらしいので、座標追従などはここで)
    force.on("tick", function() {
        link.attr({
            x1: function(data) { return data.source.x;},
            y1: function(data) { return data.source.y;},
            x2: function(data) { return data.target.x;},
            y2: function(data) { return data.target.y;}
        });
        node.attr({
            cx: function(data) { return data.x;},
            cy: function(data) { return data.y;}
        });
        // labelも追随するように
        label.attr({
            x: function(data) { return data.x;},
            y: function(data) { return data.y;}
        });
    });

</script>

</body>
</html>

1. データの用意

force layoutで遊ぶために必要な二つのデータ

force layoutで遊ぶには、以下の二つのデータが必要となる。

  • 各要素の定義(上記ソースではnodesがこれにあたる)
  • 各要素の紐付きの定義(上記ソースではlinks)

この二つさえあれば、あとはだいたいいい感じにd3.jsがやってくれるみたい。

各要素の定義

ちなみに要素に名前を表示しないで良い場合は、labelはなくてもいい。

    var nodes = [
        {id:0, label:"nodeA"},
        {id:1, label:"nodeB"},
        {id:2, label:"nodeC"},
        {id:3, label:"nodeD"},
        {id:4, label:"nodeE"},
        {id:5, label:"nodeF"},

    ];

各要素の紐付きの定義

node同士の紐付け設定。実用の際は、ここをどう作るかが難しいのかも。

    var links = [
        {source:0, target:1},
        {source:0, target:2},
        {source:1, target:3},
        {source:1, target:3},
        {source:2, target:1},
        {source:2, target:3},
        {source:3, target:4},
        {source:4, target:5},
        {source:5, target:3}
    ];

この場合は、例えばid:0のノードは1,2と紐付いており、他のnodeとは繋がっていないことがわかると思う。

2. force layoutの設定

色々な設定があるらしい。

物理に詳しくないので、色々いじってみて感覚的に理解した内容をコメントに入れておいた。

細かい内容については、公式参照。

    var force = d3.layout.force()
        .nodes(nodes)
        .links(links)
        .size([width, height])
        .distance(140) // node同士の距離
        .friction(0.9) // 摩擦力(加速度)的なものらしい。
        .charge(-100) // 寄っていこうとする力。推進力(反発力)というらしい。
        .gravity(0.1) // 画面の中央に引っ張る力。引力。
        .start();

参考

3. 描画

基本的に描画部分はsvgの領域。

今回描画するものは3点。

  • link線
  • nodeの表現(丸にしてみた)
  • ラベル表示

それぞれ特に難しいことはしていない。

実際に使う際はnodeの大きさをnodeカテゴリデータの総数などに対応させるとより直感的にわかりやすい図になるんだろうなあ。

    // link線の描画(svgのline描画機能を利用)
    var link = svg.selectAll("line")
        .data(links)
        .enter()
        .append("line")
        .style({stroke: "#ccc",
        "stroke-width": 1
    });

    // nodesの描画(今回はsvgの円描画機能を利用)
    var node = svg.selectAll("circle")
        .data(nodes)
        .enter()
        .append("circle")
        .attr({
            // せっかくなので半径をランダムに
            r: function() {return Math.random() * (40 - 10) + 10;}
        })
        .style({
            fill: "orange"
        })
        .call(force.drag);

    // nodeのラベル周りの設定
    var label = svg.selectAll('text')
        .data(nodes)
        .enter()
        .append('text')
        .attr({
            "text-anchor":"middle",
            "fill":"white",
            "font-size": "9px"
        })
        .text(function(data) { return data.label; });

4. tickイベント

作成したデモは、ノードをドラッグすることでグワングワン動かすことができる。

そこらへんを処理しているのがこのイベントとなる。

動かしたら座標を追従させるように、という設定をしている。

何か特殊な表現をしたい場合を除いてこれはもう定型文でいいと思う。

今回はラベルも表示させたかったので、追従するようにlink,nodeに習って書いたら思った通りに動いた。

直感的で非常にいいですな!!!!

    // tickイベント(力学計算が起こるたびに呼ばれるらしいので、座標追従などはここで)
    force.on("tick", function() {
        link.attr({
            x1: function(data) { return data.source.x;},
            y1: function(data) { return data.source.y;},
            x2: function(data) { return data.target.x;},
            y2: function(data) { return data.target.y;}
        });
        node.attr({
            cx: function(data) { return data.x;},
            cy: function(data) { return data.y;}
        });
        // labelも追随するように
        label.attr({
            x: function(data) { return data.x;},
            y: function(data) { return data.y;}
        });
    });

以上、d3.jsでスゴイっぽい図(force layout)を作ってみたら思ったより簡単だった件でした。