ぬるぬる動くグラフビジュアライズライブラリarbor.jsの使い方


[arbor.js](https://github.com/samizdatco/arbor)とは、下記のサンプルサイトのようなグラフが書けるライブラリです。
 - [sample](http://arborjs.org)
 - [halfviz](http://arborjs.org/halfviz/#/the-cave-of-time)

使ってみようと調べたところ、日本語の紹介サイトが少なかった[^1]ので、まとめてみました。

本稿を読めば、
![3.gif](https://qiita-image-store.s3.amazonaws.com/0/48091/bd82b144-8eae-cc3e-f00d-b8d930c3a6af.gif)
こんなのや、
![8.gif](https://qiita-image-store.s3.amazonaws.com/0/48091/dc6ef71b-8574-20cd-3e80-dee81d41ef03.gif)

こんなのが簡単に作れるようになります。

## 環境

 - Mac OSX 10.11.6
 - Safari 9.1.2
 - google Chrome 57.0.2987.133 (64-bit)
　

## 初期構成

まずは、小さいグラフでnodeを■で表示するだけの物を作ってみます。
![1.gif](https://qiita-image-store.s3.amazonaws.com/0/48091/71f20516-77e9-680d-13b4-df26ccb9f966.gif)


### ファイル配置

ライブラリは、jQueryとarbor.jsに加え、graphics.jsを使います。このファイルも[arbor.js](https://github.com/samizdatco/arbor)内においてあります。

まずは、libフォルダから、arbor.jsとarbor-tween.jsをコピーします。
<img width="414" alt="libs.png" src="https://qiita-image-store.s3.amazonaws.com/0/48091/25bb989c-2fb6-dedd-6a8c-1473ceb82c26.png">

次に、demos/_フォルダから、jQueryのファイルと、graphics.jsをコピーします。
<img width="618" alt="saport.png" src="https://qiita-image-store.s3.amazonaws.com/0/48091/f34f54ad-019d-fe76-9b4b-2343e50095cf.png">

最後に、index.htmlファイルとsample.jsファイルを作成します。ファイルの中身は、[ソースコード](ソースコード)の項に記載します。

最終的にこうなります。
<img width="403" alt="files.png" src="https://qiita-image-store.s3.amazonaws.com/0/48091/61e879fc-2b50-2579-0ce8-f3c9e03f50e0.png">



### ソースコード

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="js/jquery-1.6.1.min.js"></script>
<script src="js/jquery.address-1.4.min.js"></script>
<script src="js/arbor.js"></script>
<script src="js/arbor-tween.js"></script>
<script src="js/graphics.js"></script>
<script src="js/sample.js"></script>
<title>arbor.jsサンプル</title>
</head>
<body>
  <h1>arbor.js</h1>
<canvas id="viewport" width="280" height="280"></canvas>
</body>
</html>
```

```js:sample.js
(function($){

  var Renderer = function(canvas){
    var canvas = $(canvas).get(0)
    var ctx = canvas.getContext("2d");
    var particleSystem

    var that = {
      init:function(system){
        particleSystem = system
        particleSystem.screenSize(canvas.width, canvas.height)
        particleSystem.screenPadding(80)
        that.initMouseHandling()
      },
      redraw:function(){
        ctx.fillStyle = "white"
        ctx.fillRect(0,0, canvas.width, canvas.height)

        particleSystem.eachEdge(function(edge, pt1, pt2){
          ctx.strokeStyle = "rgba(0,0,0, .333)"
          ctx.lineWidth = 1
          ctx.beginPath()
          ctx.moveTo(pt1.x, pt1.y)
          ctx.lineTo(pt2.x, pt2.y)
          ctx.stroke()
        })

        particleSystem.eachNode(function(node, pt){
          var w = 10
          ctx.fillStyle = "rgba(0,0,0, 1)"
          ctx.fillRect(pt.x-w/2, pt.y-w/2, w,w)
        })
      },


      initMouseHandling:function(){
        var dragged = null;
        var handler = {
          clicked:function(e){
            var pos = $(canvas).offset();
            _mouseP = arbor.Point(e.pageX-pos.left, e.pageY-pos.top)
            dragged = particleSystem.nearest(_mouseP);

            if (dragged && dragged.node !== null){
              dragged.node.fixed = true
            }

            $(canvas).bind('mousemove', handler.dragged)
            $(window).bind('mouseup', handler.dropped)

            return false
          },
          dragged:function(e){
            var pos = $(canvas).offset();
            var s = arbor.Point(e.pageX-pos.left, e.pageY-pos.top)

            if (dragged && dragged.node !== null){
              var p = particleSystem.fromScreen(s)
              dragged.node.p = p
            }

            return false
          },

          dropped:function(e){
            if (dragged===null || dragged.node===undefined) return
            if (dragged.node !== null) dragged.node.fixed = false
            dragged.node.tempMass = 1000
            dragged = null
            $(canvas).unbind('mousemove', handler.dragged)
            $(window).unbind('mouseup', handler.dropped)
            _mouseP = null
            return false
          }
        }
        $(canvas).mousedown(handler.clicked);

      },

    }
    return that
  }

  $(document).ready(function(){
    var sys = arbor.ParticleSystem(1000, 60, 0.5)
    sys.parameters({gravity:true})
    sys.renderer = Renderer("#viewport")
    sys.addNode('z')
    sys.addEdge('a','b')
  })

})(this.jQuery)
```

これでひとまず、動くものが出来ました。

###  htmファイル解説
jsのライブラリを読み込んでいます。canvas上に描画するため、canvasも置いています。idは、sample.jsで利用します。

htmlファイルは、最後までこれを使います。

### jsファイル解説
-----
```js
particleSystem.eachEdge(function(edge, pt1, pt2){
```

この部分は、edge（nodeとnodeを結ぶ線）の描画処理です。この中でEdgeの見た目を決めています。今回は、半透明の黒で細い線を描画しています。

-----

```js
particleSystem.eachNode(function(node, pt)
```

この部分は、nodeの描画処理です。この中でnodeの見た目を決めています。今回は黒い小さな■を描画しています。

-----

```js
initMouseHandling:function
```

マウス操作の処理です。今回はnodeをつかんで移動させる処理が書いてあります。

----

```js
$(document).ready(function()
```

グラフの定義が書いてあります。この部分のAPIは[リファレンス](http://arborjs.org/reference)に記載がありますので、
詳しくはそちらをご確認ください。
今回は、反発：1000、張力：600、摩擦：0.5で物理世界を構築しています。
aとbが線（Edge）で繋がっているnodeでzが単独のnodeです。
適当に数値やAPIを変えると動作がわかりやすいかもしれません。

## 見た目を変更する

### node名を表示する

![2.gif](https://qiita-image-store.s3.amazonaws.com/0/48091/eac8ae7b-d76e-3416-2bb9-c68d231d4fbd.gif)


nodeの描画処理を変更します。nodeにnode名を入れるように変更しました。

宣言部分に追加します。

```js
var gfx = arbor.Graphics(canvas);
```

変える部分は、上記のnodeの描画処理の部分です。particleSystem.eachNodeに、addNodeで設定したnodeが入ってくるので、node.nameでnode名を取り出して、描画しています。

```js:sample.js
var nodeBoxes = {}
        particleSystem.eachNode(function(node, pt){
          var label = node.name||""
          var w = ctx.measureText(""+label).width + 10
          if (!(""+label).match(/^[ \t]*$/)){
            pt.x = Math.floor(pt.x)
            pt.y = Math.floor(pt.y)
          }else{
            label = null
          }
          ctx.fillStyle = "rgba(0,0,0,1)"
          gfx.rect(pt.x-w/2, pt.y-10, w,20, 4, {fill:ctx.fillStyle})
          nodeBoxes[node.name] = [pt.x-w/2, pt.y-11, w, 22]

          // draw the text
          if (label){
            ctx.font = "12px Helvetica"
            ctx.textAlign = "center"
            ctx.fillStyle = "white"
            if (node.data.color=='none') ctx.fillStyle = '#333333'
            ctx.fillText(label||"", pt.x, pt.y+4)
            ctx.fillText(label||"", pt.x, pt.y+4)
          }
        })
```

### nodeの色や形を変えてみる

![3.gif](https://qiita-image-store.s3.amazonaws.com/0/48091/bd82b144-8eae-cc3e-f00d-b8d930c3a6af.gif)


描画は、色を付けたり半透明を使ったり円を使ったり自由度が高いです。例として徳川家康の家系図を描画してみました。

```js
sys.addNode('徳川家康',{who:"ieyasu"});
sys.addNode('養珠院',{who:"sokushitsu"});
sys.addNode('宝台院',{who:"sokushitsu"});

sys.addEdge('徳川家康','養珠院');
sys.addEdge('徳川家康','頼房');
sys.addEdge('徳川家康','頼宣');
sys.addEdge('徳川家康','宝台院');
sys.addEdge('徳川家康','秀忠');
sys.addEdge('養珠院','頼房');
sys.addEdge('養珠院','頼宣');
sys.addEdge('宝台院','秀忠');
```

まずは、家系図の定義を書きます。今回は、特殊なnodeには、whoプロパティをセットしました。これを利用して形や色を変える分岐を作ります。

```js
if(node.data.who == "ieyasu")
          {
            ctx.fillStyle = "rgba(150,5,10,0.666)"
            gfx.oval(pt.x-w/2, pt.y-w/2, w,w, {fill:ctx.fillStyle})
          }
          else if(node.data.who == "sokushitsu")
          {
            ctx.fillStyle = "rgba(0,20,150,0.666)"
            gfx.rect(pt.x-w/2, pt.y-10, w,20, 4, {fill:ctx.fillStyle})
          }
          else
          {
            ctx.fillStyle = "rgba(0,0,0,0.666)"
            gfx.rect(pt.x-w/2, pt.y-10, w,20, 4, {fill:ctx.fillStyle})
          }
          nodeBoxes[node.name] = [pt.x-w/2, pt.y-11, w, 22]
```

このコードを[github](https://github.com/TakahiroYamamoto/arbor-sample)上に置きました。

### nodeを画像に置き換える

![9.gif](https://qiita-image-store.s3.amazonaws.com/0/48091/948768e1-5df7-431f-1d30-c8d8ada93d8d.gif)


canvasの画像表示機能を使います。

```sample.js
        particleSystem.eachNode(function(node, pt){
          var label = node.name||""
          var w = ctx.measureText(""+label).width + 20
          if (!(""+label).match(/^[ \t]*$/)){
            pt.x = Math.floor(pt.x)
            pt.y = Math.floor(pt.y)
          }else{
            label = null
          }

          var img = new Image();
          if(node.name == "AM")
          {
            img.src = "resource/AM.png";
            ctx.drawImage(img, pt.x-w/2, pt.y-w/2, w,w);
          }
          else if(node.name == "CH")
          {
            img.src = "resource/CH.png";
            ctx.drawImage(img, pt.x-w/2, pt.y-w/2, w,w);
          }
          else if(node.name == "CN")
          {
            img.src = "resource/CN.png";
            ctx.drawImage(img, pt.x-w/2, pt.y-w/2, w,w);
          }
          else if(node.name == "EN")
          {
            img.src = "resource/EN.png";
            ctx.drawImage(img, pt.x-w/2, pt.y-w/2, w,w);
          }
          else if(node.name == "JP")
          {
            img.src = "resource/JP.png";
            ctx.drawImage(img, pt.x-w/2, pt.y-w/2, w,w);
          }
          else
          {
            ctx.fillStyle = "rgba(0,0,0,0.666)"
            gfx.rect(pt.x-w/2, pt.y-10, w,20, 4, {fill:ctx.fillStyle})
          }
          nodeBoxes[node.name] = [pt.x-w/2, pt.y-11, w, 22]
        })
```

nodeの設定をします。

```sample.js
    var sys = arbor.ParticleSystem(1000, 60, 0.5)
    sys.parameters({gravity:true})
    sys.renderer = Renderer("#viewport")
    sys.addNode('AM');
    sys.addNode('CH');
    sys.addNode('JP');
    sys.addNode('CN');
    sys.addNode('EN');
    sys.addEdge('AM','CN');
    sys.addEdge('CN','JP');
    sys.addEdge('JP','CH');
    sys.addEdge('CH','EN');
    sys.addEdge('EN','AM');
```

srcは、[githubのリポジトリ](https://github.com/TakahiroYamamoto/arbor-sample)のimageブランチをご確認ください。
## 使用例

使いどころが難しそうなライブラリですが、分散ネットワーク（マストドンなど）を俯瞰して眺める用や、ニューラルネットの表現などにも使える可能性があるように思います。

他にも数学的な表現で使える場面があるのではないかと思います。

試しに、ある数字に割り算可能な数字が何であるかをグラフにしてみました。

![8.gif](https://qiita-image-store.s3.amazonaws.com/0/48091/dc6ef71b-8574-20cd-3e80-dee81d41ef03.gif)

srcは、[githubのリポジトリ](https://github.com/TakahiroYamamoto/arbor-sample)のgh-pagesブランチをご確認ください。

上記の動くサンプルサイトを置きました。
https://takahiroyamamoto.github.io/arbor-sample/

### アルゴリズム

```js
function factoring(seed,parent)
  {
    for(var i = Math.floor(seed/2) ; 1 < i ; i--)
    {
      var alreadyHave = false;
      if(seed % i == 0)
      {
        fact.push({'parent':parent,'seed':seed,'value':i});
        factoring(i,seed);
      }
    }
  }
```

seedの数で割り切れるiを探索し、見つけたらそれを保存して再帰探索するものです。


[^1]:
 実際に見つかったのは、この[サイト](http://pistatium.appspot.com/blog/)のみなのですが、大変参考にさせて頂きました。ありがとうございました。