D3.jsのforce layoutを使ってみる



## 概要

物理演算を使って、グラフのノードをいい感じで配置する。


## サンプル

### 基本形

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>D3.js Force Layout</title>
</head>
<body>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script>
        var width = 640;
        var height = 480;

        var svg = d3.select('body').append('svg').attr({
            width: width,
            height: height
        });

        var data = {
            nodes: [
                { name: 'aki' },
                { name: 'mii' },
                { name: 'akiton' }
            ],
            links: [
                { source: 0, target: 2 },
                { source: 1, target: 2 }
            ]
        };

        var force = d3.layout.force()
            .nodes(data.nodes)
            .links(data.links)
            .size([width, height])
            .gravity(0.1)
            .charge([-100])
            .friction(0.95)
            .linkDistance([100])
            .linkStrength(1);

        // レイアウト計算開始
        force.start();

        // lineを生成
        var line = svg.selectAll('line')
            .data(data.links)
            .enter()
            .append('line')
            .attr({
                'stroke': 'black',
                'x1': function(d, i) { return d.source.x; },
                'y1': function(d, i) { return d.source.y; },
                'x2': function(d, i) { return d.target.x; },
                'y2': function(d, i) { return d.target.y; }
            })
            .style({
            });

        var circle = svg.selectAll('circle')
            .data(data.nodes)
            .enter()
            .append('circle')
            .attr({
                'r': 8,
                'fill': 'black',
                'cx': function(d, i) { return d.x; },
                'cy': function(d, i) { return d.y; }
            })
            .style({
            })
            .call(force.drag);

        circle
            .on('mousedown', function(d, i) {
                console.log(d.name + '(' + d.x + ',' + d.y + ')');
            });

        force.on('tick', function() {
            line
                .attr({
                    'x1': function(d) { return d.source.x; },
                    'y1': function(d) { return d.source.y; },
                    'x2': function(d) { return d.target.x; },
                    'y2': function(d) { return d.target.y; }
                });
            circle
                .attr({
                    'cx': function(d) { return d.x; },
                    'cy': function(d) { return d.y; }
                });
        });
    </script>
</body>
</html>
```

### 静的に表示する場合

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>D3.js Force Layout</title>
</head>
<body>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script>
        var width = 640;
        var height = 480;

        var svg = d3.select('body').append('svg').attr({
            width: width,
            height: height
        });

        var data = {
            nodes: [
                { name: 'aki' },
                { name: 'mii' },
                { name: 'akiton' }
            ],
            links: [
                { source: 0, target: 2 },
                { source: 1, target: 2 }
            ]
        };

        var force = d3.layout.force()
            .nodes(data.nodes)
            .links(data.links)
            .size([width, height])
            .gravity(0.1)
            .charge([-100])
            .friction(0.95)
            .linkDistance([100])
            .linkStrength(1);

        // レイアウト計算開始
        force.start();
        for (var i = 0; i < 10000; i++) {
            force.tick();
        }
        force.stop();

        // lineを生成
        var line = svg.selectAll('line')
            .data(data.links)
            .enter()
            .append('line')
            .attr({
                'stroke': 'black',
                'x1': function(d, i) { return d.source.x; },
                'y1': function(d, i) { return d.source.y; },
                'x2': function(d, i) { return d.target.x; },
                'y2': function(d, i) { return d.target.y; }
            })
            .style({
            });

        var circle = svg.selectAll('circle')
            .data(data.nodes)
            .enter()
            .append('circle')
            .attr({
                'r': 8,
                'fill': 'black',
                'cx': function(d, i) { return d.x; },
                'cy': function(d, i) { return d.y; }
            })
            .style({
            })
            .call(force.drag);
    </script>
</body>
</html>
```

### 静的レイアウト後にしょぼいドラッグ追加

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>D3.js Force Layout</title>
</head>
<body>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script>
        var width = 640;
        var height = 480;

        var svg = d3.select('body').append('svg').attr({
            width: width,
            height: height
        });

        var data = {
            nodes: [
                { name: 'aki' },
                { name: 'mii' },
                { name: 'akiton' }
            ],
            links: [
                { source: 0, target: 2 },
                { source: 1, target: 2 }
            ]
        };

        var force = d3.layout.force()
            .nodes(data.nodes)
            .links(data.links)
            .size([width, height])
            .gravity(0.1)
            .charge([-100])
            .friction(0.95)
            .linkDistance([100])
            .linkStrength(1);

        // レイアウト計算開始
        force.start();
        for (var i = 0; i < 10000; i++) {
            force.tick();
        }
        force.stop();

        // lineを生成
        var line = svg.selectAll('line')
            .data(data.links)
            .enter()
            .append('line')
            .attr({
                'stroke': 'black',
                'x1': function(d, i) { return d.source.x; },
                'y1': function(d, i) { return d.source.y; },
                'x2': function(d, i) { return d.target.x; },
                'y2': function(d, i) { return d.target.y; }
            })
            .style({
            });

        var circle = svg.selectAll('circle')
            .data(data.nodes)
            .enter()
            .append('circle')
            .attr({
                'r': 8,
                'fill': 'black',
                'cx': function(d, i) { return d.x; },
                'cy': function(d, i) { return d.y; }
            })
            .style({
            });

        var moveTarget = null;
        circle
            .on('mousedown', function(d, i) {
                console.log(d.name + '(' + d.x + ',' + d.y + ')');
                moveTarget = d;
            })
            .on('mouseup', function(d, i) {
                moveTarget = null;
            });

        svg.on('mousemove', function(e) {
            if (moveTarget != null) {
                var mousePos = d3.mouse(this);
                console.log('moveTarget = ' + moveTarget.name + ' (' + mousePos[0] + ',' + mousePos[1] + ')');
                moveTarget.x = mousePos[0];
                moveTarget.y = mousePos[1];

                line
                    .attr({
                        'x1': function(d) { return d.source.x; },
                        'y1': function(d) { return d.source.y; },
                        'x2': function(d) { return d.target.x; },
                        'y2': function(d) { return d.target.y; }
                    });
                circle
                    .attr({
                        'cx': function(d) { return d.x; },
                        'cy': function(d) { return d.y; }
                    });
            }
        });
    </script>
</body>
</html>
```

### データを複数オブジェクトにバインド・データの追加

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>D3.js Force Layout</title>
</head>
<body>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script>
        var width = 640;
        var height = 480;

        var svg = d3.select('body').append('svg').attr({
            width: width,
            height: height
        });

        var data = {
            nodes: [
                { name: 'aki' },
                { name: 'mii' },
                { name: 'akiton' }
            ],
            links: [
                { source: 0, target: 2 },
                { source: 1, target: 2 }
            ]
        };

        var force = d3.layout.force()
            .nodes(data.nodes)
            .links(data.links)
            .size([width, height])
            .gravity(0.1)
            .charge([-100])
            .friction(0.95)
            .linkDistance([100])
            .linkStrength(1);

        // レイアウト計算開始
        force.start();
        for (var i = 0; i < 10000; i++) {
            force.tick();
        }
        force.stop();

        // lineを生成
        var line = svg.selectAll('line')
            .data(data.links)
            .enter()
            .append('line')
            .attr({
                'stroke': 'black',
                'x1': function(d, i) { return d.source.x; },
                'y1': function(d, i) { return d.source.y; },
                'x2': function(d, i) { return d.target.x; },
                'y2': function(d, i) { return d.target.y; }
            })
            .style({
            });

        var circle = svg.selectAll('circle')
            .data(data.nodes)
            .enter()
            .append('circle')
            .attr({
                'r': 8,
                'fill': 'black',
                'cx': function(d, i) { return d.x; },
                'cy': function(d, i) { return d.y; }
            })
            .style({
            });
            // .call(force.drag);

        var moveTarget = null;
        circle
            .on('mousedown', function(d, i) {
                console.log(d.name + '(' + d.x + ',' + d.y + ')');
                moveTarget = d;
            })
            .on('mouseup', function(d, i) {
                moveTarget = null;
            });

        svg.on('mousemove', function(e) {
            if (moveTarget != null) {
                var mousePos = d3.mouse(this);
                console.log('moveTarget = ' + moveTarget.name + ' (' + mousePos[0] + ',' + mousePos[1] + ')');
                moveTarget.x = mousePos[0];
                moveTarget.y = mousePos[1];

                d3.selectAll('line')
                    .attr({
                        'x1': function(d) { return d.source.x; },
                        'y1': function(d) { return d.source.y; },
                        'x2': function(d) { return d.target.x; },
                        'y2': function(d) { return d.target.y; }
                    });
                d3.selectAll('circle')
                    .attr({
                        'cx': function(d) { return d.x; },
                        'cy': function(d) { return d.y; }
                    });
            }
        });

        svg.on('mousedown', function() {
            if (moveTarget != null) return;
            var newCircle = svg.append('circle');
            var mousePos = d3.mouse(this);
            var newData = {
                name: 'super',
                age: 10,
                x: mousePos[0],
                y: mousePos[1]
            };
            data.nodes.push(newData);
            console.log('add circle: ' + newData.x + ',' + newData.y);
            newCircle
                .datum(newData)
                .attr({
                    'cx': function(d, i) { console.log(d);return d.x; },
                    'cy': function(d, i) { return d.y; },
                    'r': 10,
                    'fill': 'red'
                })
                .on('mousedown', function(d) {
                    moveTarget = d;
                })
                .on('mouseup', function(d) {
                    moveTarget = null;
                });
            svg.append('text')
                .datum(newData)
                .text(function(d, i) { return d.name; })
                .attr({
                    'font-size': '32',
                    'fill': 'white',
                    'stroke': 'black',
                    'x': function(d, i) { return d.x; },
                    'y': function(d, i) { return d.y; }
                });
        });

        function randomMove() {
            for (var i = 0; i < data.nodes.length; i++) {
                var d = data.nodes[i];
                d.x += Math.random() * 2 <= 1 ? Math.random()*10 : -Math.random()*10;
                d.y += Math.random() * 2 <= 1 ? Math.random()*10 : -Math.random()*10;

                d3.selectAll('line')
                    .attr({
                        'x1': function(d) { return d.source.x; },
                        'y1': function(d) { return d.source.y; },
                        'x2': function(d) { return d.target.x; },
                        'y2': function(d) { return d.target.y; }
                    });
                d3.selectAll('circle')
                    .attr({
                        'cx': function(d) { return d.x; },
                        'cy': function(d) { return d.y; }
                    });
                d3.selectAll('text')
                    .attr({
                        'x': function(d) { return d.x; },
                        'y': function(d) { return d.y; }
                    });
            }
        }
    </script>
    <input type="button" value="randomMove" onClick="randomMove()"/>
</body>
</html>
```


## 参考

- [D3.jsのforce layoutで遊ぶ](http://lisia.hatenadiary.jp/entry/2014/07/11/225904)
- [【D3.js】ノードをドラッグできる静的なForceレイアウト](http://shimz.me/blog/d3-js/3803)
- [マウス・イベントやタッチ・イベントの扱い方](http://koyamatch.com/d3js/d3js11.html)
- [D3.jsのForce Layout (力学モデルでグラフ描画するレイアウト)を動かしてみた](http://totech.hateblo.jp/entry/2014/11/18/104137)
- [セレクションの仕組み](http://ja.d3js.info/mike/selection/)