動的なチャートでオブジェクト同士の関係性を可視化 ‐ Yellowfin と amCharts の連携

## はじめに
[amCharts](https://www.amcharts.com/) という JavaScript ライブラリを活用すると、色々と動きの面白いチャートを作成することができます。
今回はその中から、Directed Tree のチャートを使った例を紹介したいと思います。
![hierarchy (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/01640552-f8bd-44a3-8917-6576a44a6827.gif)

## 事前準備
#### 1-1. ビューの作成まで
[以前の記事](https://qiita.com/ExtremeCarvFJ/items/ad453a6110cec093bdb9)  を参考に、『1-2. ビューの作成』 までの作業を実施します。
今回使うデータは以下の表です。飲料製品の、各カテゴリとブランドのつながりを示すものとお考え下さい。

|category|bland|value|
|---|---|---|
|コーヒー|コナ|37|
|コーヒー|モカ|37|
|コーヒー|ブレンド|41|
|コーヒー|トラジャ|43|
|紅茶|アッサム|42|
|紅茶|ウヴァ|37|
|紅茶|ジャワ|38|
|紅茶|セイロン|42|
|日本茶|緑茶|39|
|日本茶|ほうじ茶|42|
|日本茶|麦茶|39|
|日本茶|玄米茶|39|
|ソーダ|ファンタ|40|
|ソーダ|コーラ|38|
|ソーダ|サイダー|41|
|ソーダ|強炭酸|41|

## 2. グラフの作成
準備が整ったら、グラフの作成に手順を進めます。[新規でレポートを作成](https://wiki.yellowfin.co.jp/space/yfcurrent/1904543/%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88&%E3%82%B0%E3%83%A9%E3%83%95%E3%81%AE%E4%BD%9C%E6%88%90)し、以下の手順に従ってグラフを作成します。
#### 2-1. [データ] ステップ
ビューに設定した軸と集計値をテーブルに配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/f33e151b-a542-4ef6-be2b-c8ac81b9a82d.png)

#### 2-2. [グラフ] ステップ
[グラフ] ステップに進み、画面右側 [グラフの選択] から [JavaScriptグラフ] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/a945db8d-1d54-4647-bdec-c1305463120a.png)

##### JavaScriptタブ
雛形を全て削除し、以下のコードに置き換えます。コードの処理の内容は後ほど説明します。
```javascript:全容
generateChart = function(options) {
   var $chartDrawDiv = $(options.divSelector);
   var processedCoffee = processCoffee(options.dataset.data);
   var processedTea = processTea(options.dataset.data);
   var processedJapan = processJapan(options.dataset.data);
   var processedSoda = processSoda(options.dataset.data);
   doDrawing($chartDrawDiv, processedCoffee, processedTea, processedJapan, processedSoda);
},

processCoffee = function(dataset) {
    var ds_coffee = [];
    for (i=0;i<dataset.category.length;i++){
        if(dataset.category[i].raw_data === "コーヒー"){
            ds_coffee.push({
                name: dataset.bland[i].raw_data,
                value: dataset.value[i].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_coffee));
    return ds_coffee;
},

processTea = function(dataset) {
    var ds_tea = [];
    for (ii=0;ii<dataset.category.length;ii++){
        if(dataset.category[ii].raw_data === "紅茶"){
            ds_tea.push({
                name: dataset.bland[ii].raw_data,
                value: dataset.value[ii].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_tea));
    return ds_tea;
},

processJapan = function(dataset) {
    var ds_japan = [];
    for (iii=0;iii<dataset.category.length;iii++){
        if(dataset.category[iii].raw_data === "日本茶"){
            ds_japan.push({
                name: dataset.bland[iii].raw_data,
                value: dataset.value[iii].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_japan));
    return ds_japan;
},

processSoda = function(dataset) {
    var ds_soda = [];
    for (iiii=0;iiii<dataset.category.length;iiii++){
        if(dataset.category[iiii].raw_data === "ソーダ"){
            ds_soda.push({
                name: dataset.bland[iiii].raw_data,
                value: dataset.value[iiii].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_soda));
    return ds_soda;
},

doDrawing = function($chartDrawDiv, ds_coffee, ds_tea, ds_japan, ds_soda) {
    require(['https://cdn.amcharts.com/lib/5/index.js','https://cdn.amcharts.com/lib/5/hierarchy.js','https://cdn.amcharts.com/lib/5/themes/Animated.js'], function(){
        var $canvas = $('<div id="chartdiv"></div>');
        $chartDrawDiv.append($canvas);

        am5.ready(function() {
            var root = am5.Root.new("chartdiv");
        
        root.setThemes([
          am5themes_Animated.new(root)
        ]);
        
        var data = {
          value: 0,
          children: [
            {
              name: "コーヒー",
              children: ds_coffee
            },
            {
              name: "紅茶",
              children: ds_tea
            },
            {
              name: "日本茶",
              children: ds_japan
            },
            {
              name: "ソーダ",
              children: ds_soda
            }
          ],
        };
        
        var zoomableContainer = root.container.children.push(
          am5.ZoomableContainer.new(root, {
            width: am5.p100,
            height: am5.p100,
            wheelable: true,
            pinchZoom: true
          })
        );
        
        var zoomTools = zoomableContainer.children.push(am5.ZoomTools.new(root, {
          target: zoomableContainer
        }));
        
        var series = zoomableContainer.contents.children.push(am5hierarchy.ForceDirected.new(root, {
          maskContent:false,
          singleBranchOnly: false,
          downDepth: 2,
          topDepth: 1,
          initialDepth: 3,
          valueField: "value",
          categoryField: "name",
          childDataField: "children",
          idField: "name",
          linkWithField: "linkWith",
          manyBodyStrength: -10,
          centerStrength: 0.8
        }));
        
        series.get("colors").setAll({
          step: 2
        });
        series.links.template.set("strength", 0.5);
        series.labels.template.set("minScale", 0);
        series.data.setAll([data]);
        series.set("selectedDataItem", series.dataItems[0]);
        series.appear(1000, 100);
        
        });
    });
};
```

## コード

##### generateChart
```JavaScript:generateChart
generateChart = function(options) {
   var $chartDrawDiv = $(options.divSelector);
   var processedCoffee = processCoffee(options.dataset.data);
   var processedTea = processTea(options.dataset.data);
   var processedJapan = processJapan(options.dataset.data);
   var processedSoda = processSoda(options.dataset.data);
   doDrawing($chartDrawDiv, processedCoffee, processedTea, processedJapan, processedSoda);
},
```

JavaScript グラフの処理において、最初に呼び出されるのが generateChart = function(options) です。
[データ] ステップでテーブルに追加した情報が options に格納されて、メソッドに受け渡されます。
processCoffee、ProcessTea、ProcessJapan、ProcessSoda でデータ処理を行うメソッドを呼び出し、doDrawingで描画するメソッドをそれぞれ呼び出しています。各メソッドで実行する具体的な処理は後述します。

##### processCoffee
```JavaScript:processCoffee
processCoffee = function(dataset) {
    var ds_coffee = [];
    for (i=0;i<dataset.category.length;i++){
        if(dataset.category[i].raw_data === "コーヒー"){
            ds_coffee.push({
                name: dataset.bland[i].raw_data,
                value: dataset.value[i].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_coffee));
    return ds_coffee;
},
```
コーヒーのツリーに結び付けるデータを、配列の形でデータセットに格納します。name にブランドを、value に売上を格納します。
`console.log(JSON.stringify(ds_coffee))` を仕込んでおくと、下のようにブラウザのコンソールで具体的なデータを確認することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/9a694e3b-0648-4fe2-a146-ab2eda94dcf8.png)

##### processTea
```JavaScript:processTea
processTea = function(dataset) {
    var ds_tea = [];
    for (ii=0;ii<dataset.category.length;ii++){
        if(dataset.category[ii].raw_data === "紅茶"){
            ds_tea.push({
                name: dataset.bland[ii].raw_data,
                value: dataset.value[ii].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_tea));
    return ds_tea;
},
```

同様に、紅茶のツリーに結び付けるデータを、配列の形でデータセットに格納します。

##### processJapan
```JavaScript:processJapan
processJapan = function(dataset) {
    var ds_japan = [];
    for (iii=0;iii<dataset.category.length;iii++){
        if(dataset.category[iii].raw_data === "日本茶"){
            ds_japan.push({
                name: dataset.bland[iii].raw_data,
                value: dataset.value[iii].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_japan));
    return ds_japan;
},
```

次に日本茶です。

##### processSoda
```JavaScript:processSoda
processSoda = function(dataset) {
    var ds_soda = [];
    for (iiii=0;iiii<dataset.category.length;iiii++){
        if(dataset.category[iiii].raw_data === "ソーダ"){
            ds_soda.push({
                name: dataset.bland[iiii].raw_data,
                value: dataset.value[iiii].raw_data,
            });
        }
    }
    console.log(JSON.stringify(ds_soda));
    return ds_soda;
},
```

最後にソーダです。

##### CSS
```css:css
#chartdiv{
    height: 500px;
    width: 100%;
}
```

CSS タブに上記を記述します。後ほど、定義した id を JavaScript から呼び出します。

##### doDrawing
```JavaScript:doDrawing
doDrawing = function($chartDrawDiv, ds_coffee, ds_tea, ds_japan, ds_soda) {
    require(['https://cdn.amcharts.com/lib/5/index.js','https://cdn.amcharts.com/lib/5/hierarchy.js','https://cdn.amcharts.com/lib/5/themes/Animated.js'], function(){
        var $canvas = $('<div id="chartdiv"></div>');
        $chartDrawDiv.append($canvas);

        am5.ready(function() {
                var root = am5.Root.new("chartdiv");
        
        root.setThemes([
          am5themes_Animated.new(root)
        ]);
        
        var data = {
          value: 0,
          children: [
            {
              name: "コーヒー",
              children: ds_coffee
            },
            {
              name: "紅茶",
              children: ds_tea
            },
            {
              name: "日本茶",
              children: ds_japan
            },
            {
              name: "ソーダ",
              children: ds_soda
            }
          ],
        };
        
        var zoomableContainer = root.container.children.push(
          am5.ZoomableContainer.new(root, {
            width: am5.p100,
            height: am5.p100,
            wheelable: true,
            pinchZoom: true
          })
        );
        
        var zoomTools = zoomableContainer.children.push(am5.ZoomTools.new(root, {
          target: zoomableContainer
        }));
        
        var series = zoomableContainer.contents.children.push(am5hierarchy.ForceDirected.new(root, {
          maskContent:false,
          singleBranchOnly: false,
          downDepth: 2,
          topDepth: 1,
          initialDepth: 3,
          valueField: "value",
          categoryField: "name",
          childDataField: "children",
          idField: "name",
          linkWithField: "linkWith",
          manyBodyStrength: -10,
          centerStrength: 0.8
        }));
        
        series.get("colors").setAll({
          step: 2
        });
        series.links.template.set("strength", 0.5);
        series.labels.template.set("minScale", 0);
        series.data.setAll([data]);
        series.set("selectedDataItem", series.dataItems[0]);
        series.appear(1000, 100);
        
        });
    });
};
```

`require[]` の中で、JavaScriptライブラリの場所を指定します。上記の例では CDN のアドレスを指定していますが、JS ライブラリを[ダウンロード](https://www.amcharts.com/download/)して利用することも可能です。
`var root = am5.Root.new("chartdiv")` で、CSS で定義した id を指定します。
`root.setThemes` で、デザインのテーマを指定します。今回は動きのある Animated を指定していますが、その他にも[こちら](https://www.amcharts.com/docs/v5/concepts/themes/)のテーマが指定可能です。
`var data = {}`の中で、作成したデータセットを、描画用のメソッドに受け渡しています。
`var zoomableContainer` と `var zoomTools` で、マウス操作および画面上のアイコンによるズームを可能にします。
`var series` 以降で、表示内容やフィールド名称などに関する一通りの設定を設定し、適用しています。各設定の詳細は[こちら](https://www.amcharts.com/docs/v5/charts/hierarchy/#Adding)でご確認ください。

##### 出来上がり
結果、以下のようなチャートが出来上がりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/bd360c25-f481-49ef-8bf7-a0006f42f306.png)

## 最後に
オブジェクト同士のつながりは、やはり見やすいチャートで可視化したいものですね。
何より動きがあるチャートは見栄えが良い。
具体的な用途としては、人と人との関係性を可視化することなどにも使えそうですね。例えば親族同士のつながりを可視化して、インサイダー取引の疑いがある時に確認するなど。
他にも考えられ得る使用方法があれば是非ご意見ください。

では皆様、良いデータ分析を！