雑食性雑感雑記

知識の整理場。ため込んだ知識をブログ記事として再構築します。

D3.js の force layout を試してみる

概要

  • D3.js で Force layout を動かしてみる。
    • 理解用に簡単なサンプルを作る。
  • その他、オプションを試してみる。
    • ラベル表示と矢印を追加。

情報

D3.js って…?

  • 日本語サイト
  • D3 = Data Driven Document
    • データに基づいてドキュメント (要は DOM) を操作するための Javascriptライブラリ。
  • svg を使った華麗なグラフのデモが目立つが、DOM操作のライブラリとしても優れている (と、使ってみて思った)。

Force layout

  • force = 『力』とか『エネルギー』とか。
    • スターウォーズのアレ??
  • 要素同士が影響し合っている状態を node (円) と link (線) で表している。
  • 説明よりもサンプル見た方が早い。

作ったサンプル

  • jsdo.it 上に置きました。


  • 作り方とかは以下、詳細に。

簡単なサンプル作ってみる

データ

  • 『AはBを好き』というデータを適当に作ってみた。
No. 名前 性別 好き (No.)
1 陽菜 (6)
2 大翔 結菜 (8)
3 陽向 葵 (9)
4 陽太 結菜 (8)
5 悠真 結愛 (10)
6 陽菜 陽向 (3)
7 悠真 (5)
8 結菜 大翔 (2)
9 悠真 (5)
10 結愛 蓮 (1)

  • 名前は2014年の名前ランキング上位から取得。他意は無いです。
  • 好き合っているのは乱数で設定。両想い分だけ手作業。

データを json 化

  • Javascript で扱うにあたり、JSON形式が読みやすい。
  • Force layout のサンプルに従い、手作業でJSON作成。
  • data.json
{
    "nodes":[
        {"name": "蓮", "group": 1},
        {"name": "大翔", "group": 1},
        {"name": "陽向", "group": 1},
        {"name": "陽太", "group": 1},
        {"name": "悠真", "group": 1},
        {"name": "陽菜", "group": 2},
        {"name": "凛", "group": 2},
        {"name": "結菜", "group": 2},
        {"name": "葵", "group": 2},
        {"name": "結愛", "group": 2}
    ],
    "links":[
        {"source": 0, "target": 5},
        {"source": 1, "target": 7},
        {"source": 2, "target": 8},
        {"source": 3, "target": 7},
        {"source": 4, "target": 9},
        {"source": 5, "target": 2},
        {"source": 6, "target": 4},
        {"source": 7, "target": 1},
        {"source": 8, "target": 4},
        {"source": 9, "target": 0}
    ]
}
  • メモ
    • group は後で色分けに使用。性別で振った。
    • links の source、target は nodes の並び順 (0 indexed)。

HTML、CSS、Javascript 準備

  • 構成
  index.html
  - js
      - main.js
  - css
      - style.css
  - libs
      - d3.v3.min.js
  - data
      - data.json

  • index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8"/>
        <title>d3.js sample</title>
        
        <!-- styles -->
        <link rel="stylesheet" href="css/style.css"/>
    </head>
    <body>
        <div class="svg_area" id="svg_area"></div>
        
        <!-- libs -->
        <script type="text/javascript" src="libs/d3.v3.min.js"></script>
        <!-- scripts -->
        <script type="text/javascript" src="js/main.js"></script>
    </body>
</html>

  • style.css
/*** Field ***/
.svg_area {
    position: relative;
    width: 500px;
    height: 500px;
    border: dashed 5px #000;
}

/*** Force layout ***/
.node {
    stroke: #fff;
    stroke-width: 1.5px;
}

.link {
    stroke: #999;
    stroke-opacity: .6;
}
  • メモ
    • 表示エリアのサイズと、force layout の設定。

  • main.js
/**
 * Initialize SVG display
 */
function init(svg_id, width, height, data_path)
{
    // 色は既定のがある。
    var color = d3.scale.category20();

    var force = d3.layout.force()
                  .charge(-500)      // node同士の力の基準?
                  .linkDistance(100)  // node同士の距離の基準
                  .size([width, height]);

    var svg = d3.select('#' + svg_id).append('svg')
                .attr('width', width)
                .attr('height', height);

    d3.json(data_path, function(error, graph) {

        // JSON read error 時
        if (graph == null) {
            alert(error);
            return;
        }

        force.nodes(graph.nodes)
             .links(graph.links);

        // Tick start
        force.start();

        var link = svg.selectAll('.link')
                      .data(graph.links).enter()
                      .append('line')
                      .attr('class', 'link')
                      .style('stroke-width', 1);

        var node = svg.selectAll('.node')
                      .data(graph.nodes).enter()
                      .append('circle')
                      .attr('class', 'node')
                      .attr('r', 5)
                      .style('fill', function(d) { return color(d.group); })
                      .call(force.drag);

        force.on('tick', function() {
            link.attr('x1', function(d) { return d.source.x; })
                .attr('y1', function(d) { return d.source.y; })
                .attr('x2', function(d) { return d.target.x; })
                .attr('y2', function(d) { return d.target.y; });

            node.attr('cx', function(d) { return d.x; })
                .attr('cy', function(d) { return d.y; });
        });
    });
}

/**
 * Main
 */
var svg_id    = 'svg_area';
var element   = document.getElementById(svg_id);
var data_path = 'data/data.json';

init(svg_id, element.clientWidth, element.clientHeight, data_path);
  • メモ
    • force layout は「d3.layout.force()」にデータ与えれば自動生成してくれる。便利!!
      • tick 動かして Update 処理とか、JSON の形式とか、色々制約はあるけど…。

オプション表示

ラベル付与

  • css.style に text のスタイル追加
.node text {
    font: 16px helvetica;
}

  • main.js (変更主要部分)
    d3.json(data_path, function(error, graph) {

        // 略

        var node = svg.selectAll('.node')
                      .data(graph.nodes).enter()
                      .append('g')
                      .attr('class', 'node')
                      .call(force.drag);

        var circle = node.append('circle')
                         .attr('r', 5)
                         .style('fill', function(d) { return color(d.group); });

        var text = node.append('text')
                       .attr('dx', 10)
                       .attr('dy', '.35em')
                       .text(function(d) { return d.name; })
                       .style('stroke', 'gray');

        force.on('tick', function() {
            // 略 (link 位置更新)

            circle.attr('cx', function(d) { return d.x; })
                  .attr('cy', function(d) { return d.y; });

            text.attr('x', function(d) { return d.x; })
                .attr('y', function(d) { return d.y; });
        });
    });
  • メモ
    • 元は '.node' 直下に circle 作っていたが、変更。
      • '.node' 直下に 'g' 作って、node 属性複数個。
      • その下に 'circle' と ’text' 付加
    • tick 時処理も circle と text 両方更新

矢印追加

  • source から target に向かう矢印表示
  • svg の defs に矢印表示を定義し、それを各 link に当てる形の様だ。

  • main.js (主要部分)
function _setArrow(svg)
{
    svg.append('defs').selectAll('marker')
       .data(['arrow']).enter()
       .append('marker')
       .attr('id', function(d) { return d; })
       .attr('viewBox', '0 -5 10 10')
       .attr('refX', 25)
       .attr('refY', 0)
       .attr('markerWidth', 10)
       .attr('markerHeight', 10)
       .attr('orient', 'auto')
       .append('path')
       .attr('d', 'M0,-5L10,0L0,5 L10,0 L0, -5')
       .style('stroke', '#4679BD')
       .style('opacity', '0.6');
};

function init(svg_id, width, height, data_path)
{
    // 略

    var svg = d3.select('#' + svg_id).append('svg')
                .attr('width', width)
                .attr('height', height);

    _setArrow(svg);

    d3.json(data_path, function(error, graph) {

        // 略

        var link = svg.selectAll('.link')
                      .data(graph.links).enter()
                      .append('line')
                      .attr('class', 'link')
                      .style('stroke-width', 1)
                      .style('marker-end', 'url(#arrow)');

        // 略
    });
}