ベン図 ‐ Yellowfin と amCharts の連携

## はじめに
[Yellowfin](https://yellowfin.co.jp/) と [amCharts](https://www.amcharts.com/) JavaScript ライブラリの活用第2弾として、今回はベン図を作成したいと思います。ちなみに、第一弾は[こちら](https://qiita.com/ExtremeCarvFJ/items/214be1ac00f7f56eb44f)。

## 事前準備
#### 1-1. ビューの作成まで
[以前の記事](https://qiita.com/ExtremeCarvFJ/items/ad453a6110cec093bdb9)  を参考に、『1-2. ビューの作成』 までの作業を実施します。
今回ビューが参照するテーブルのデータは以下です。犬好きか、猫好きか、あるいは両方好きかのアンケート結果とお考え下さい。

|name|dog|cat|
|---|---|---|
|ゆい|はい|いいえ|
|たろう|いいえ|はい|
|みなみ|はい|いいえ|
|しょうのすけ|はい|いいえ|
|ななえ|いいえ|はい|
|ステファン|はい|はい|

上記テーブルの内容を、[参照コード](https://wiki.yellowfin.co.jp/space/yfcurrent/1903986/%E5%8F%82%E7%85%A7%E3%82%B3%E3%83%BC%E3%83%89)を使って以下のように修正します。dog 列の「はい」を「犬」に、cat 列の「はい」を「猫」に置き換えているだけです。
|name|dog|cat|
|---|---|---|
|ゆい|犬|いいえ|
|たろう|いいえ|猫|
|みなみ|犬|いいえ|
|しょうのすけ|犬|いいえ|
|ななえ|いいえ|猫|
|ステファン|犬|猫|

## 2. グラフの作成
準備が整ったら、グラフの作成に手順を進めます。新規でレポートを作成し、以下の手順に従ってグラフを作成します。
#### 2-1. [データ] ステップ
dog、cat、name 列をテーブルに配置し、name の個数を count するテーブルを作成します。
Yellowfin の集約に関しては、[こちら](https://wiki.yellowfin.co.jp/space/yfcurrent/1901411/%E9%9B%86%E7%B4%84)をご参照ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/2fa7b7bf-cc1b-486c-aa85-9f9803c80dd6.png)
 
#### 2-2. [グラフ] ステップ
[グラフ] ステップに進み、画面右側 [グラフの選択] から [JavaScriptグラフ] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/a516a71c-b39b-44a7-b459-2f5580799ea8.png)

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

processData = function(dataset) {
    var ds = [];
    for (i=0;i<dataset.dog.length;i++){
        if(dataset.dog[i].raw_data == 'はい' && dataset.cat[i].raw_data == 'いいえ'){
            var sets1 = [];
            sets1.push(dataset.dog[i].formatted_data);
            ds.push({
                name: dataset.dog[i].formatted_data,
                value: dataset.name[i].formatted_data,
                sets: sets1,
                sliceSettings: {
                    fill: am5.color(0x6e5a40)
                }
            });
        } else if(dataset.dog[i].raw_data == 'いいえ' && dataset.cat[i].raw_data == 'はい'){
            var sets2 = [];
            sets2.push(dataset.cat[i].formatted_data);
            ds.push({
                name: dataset.cat[i].formatted_data,
                value: dataset.name[i].formatted_data,
                sets: sets2,
                sliceSettings: {
                    fill: am5.color(0xb1d245),
                }
            });
        } else if(dataset.dog[i].raw_data == 'はい' && dataset.cat[i].raw_data == 'はい'){
            var sets3 = [];
            sets3.push(dataset.dog[i].formatted_data);
            sets3.push(dataset.cat[i].formatted_data);
            ds.push({
                name: "両方",
                value: dataset.name[i].formatted_data,
                sets: sets3,
                sliceSettings: {
                    fill: am5.color(0x5aaa95),
                }
            });
        }
    }
    console.log(JSON.stringify(ds));
    return ds;
},

doDrawing = function($chartDrawDiv, ds, obj) {
    require(['https://cdn.amcharts.com/lib/5/index.js','https://cdn.amcharts.com/lib/5/venn.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 container = root.container.children.push(am5.Container.new(root, {
              width: am5.p100,
              height: am5.p100,
              layout: root.verticalLayout
            }));
            
            var chart = container.children.push(am5venn.Venn.new(root, {
              categoryField: "name",
              valueField: "value",
              intersectionsField: "sets",
              paddingTop: 40,
              paddingBottom: 40,
              paddingLeft: 40,
              paddingRight: 40
            }));
            
            var pattern = am5.CirclePattern.new(root, {
              fill: am5.color(0x000000),
              color: am5.color(0xffffff),
              radius: 10,
              gap: 10,
              checkered: true
            })
            
            chart.slices.template.setAll({ templateField: "sliceSettings" });
            chart.labels.template.set("fill", am5.color(0xffffff));
            chart.labels.template.setup = function(target) {
              target.set("background", am5.RoundedRectangle.new(root, {
                stroke: am5.color(0xffffffff),
                fill: am5.color(0x000000),
                cornerRadiusTL: 5,
                cornerRadiusTR: 5,
                cornerRadiusBL: 5,
                cornerRadiusBR: 5,
                fillOpacity: 1
              }));
            };
            
            chart.data.setAll(ds);

            chart.hoverGraphics.setAll({
              strokeDasharray: [3, 3],
              stroke: am5.color(0xffffff),
              strokeWidth: 2
            });
        });
    });
};
```

プレビューをすると、以下のようなベン図が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/c056c568-bd6c-4630-afc9-3a6d3cd0904d.png)

## コード

コードの中身を、順を追って説明します。

##### generateChart
```JavaScript:generateChart
generateChart = function(options) {
   var $chartDrawDiv = $(options.divSelector);
   var processedData = processData(options.dataset.data);
   doDrawing($chartDrawDiv, processedData);
},
```

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

##### processData
```JavaScript:processData
processData = function(dataset) {
    var ds = [];
    for (i=0;i<dataset.dog.length;i++){
        if(dataset.dog[i].raw_data == 'はい' && dataset.cat[i].raw_data == 'いいえ'){
            var sets1 = [];
            sets1.push(dataset.dog[i].formatted_data);
            ds.push({
                name: dataset.dog[i].formatted_data,
                value: dataset.name[i].formatted_data,
                sets: sets1,
                sliceSettings: {
                    fill: am5.color(0x6e5a40)
                }
            });
        } else if(dataset.dog[i].raw_data == 'いいえ' && dataset.cat[i].raw_data == 'はい'){
            var sets2 = [];
            sets2.push(dataset.cat[i].formatted_data);
            ds.push({
                name: dataset.cat[i].formatted_data,
                value: dataset.name[i].formatted_data,
                sets: sets2,
                sliceSettings: {
                    fill: am5.color(0xb1d245),
                }
            });
        } else if(dataset.dog[i].raw_data == 'はい' && dataset.cat[i].raw_data == 'はい'){
            var sets3 = [];
            sets3.push(dataset.dog[i].formatted_data);
            sets3.push(dataset.cat[i].formatted_data);
            ds.push({
                name: "両方",
                value: dataset.name[i].formatted_data,
                sets: sets3,
                sliceSettings: {
                    fill: am5.color(0x5aaa95),
                }
            });
        }
    }
    console.log(JSON.stringify(ds));
    return ds;
},
```

processData メソッドで、下記データが生成されます。

```JSON:data
[{
"name": "猫",
	"value": 2,
	"sets": ["猫"],
	"sliceSettings": {"fill":{"_hex":11653701}
},{
	"name": "犬",
	"value": 3,
	"sets": ["犬"],
	"sliceSettings": {"fill":{"_hex":7232064}
},{
	"name": "両方",
	"value": 1,
	"sets": ["犬","猫"],
	"sliceSettings": {"fill":{"_hex":5941909}
}
}]
```

* if で 3 条件に分けて、データセットを作成しています。if 条件の中では、raw_data を使って条件分岐を行っています。raw_data は、参照コードで値を置き替える前の元データを参照するときは、raw_data を指定します
* sets キーの値は、[] で囲まれた配列の形になっています。つまり、JSON 全体としては、配列の入れ子構造になっています。そのため、予め set1 = で配列の値を作成しておいて、後に親の JSON データの sets の値に指定しています
* sets1.push や ds.push などの中では、配列に formatted_data を追加しています。formatted_data は参照コードで値を置き替えた後のデータを参照する時に指定します
* sliceSettings キーの値には、丸の内部を塗りつぶす色を固定値で設定しています。もちろん、データベースから色を定義する 16 進数のデータを取得して、ここに設定しても大丈夫です。sliceSettings で設定できる値一覧は[こちら](https://www.amcharts.com/docs/v5/reference/slice/)でご確認ください
* 生成されるデータの中身を確認したい場合は、`console.log(JSON.stringify(ds_coffee))` で、データをコンソールに出力しておくと便利です

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

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

##### doDrawing
```JavaScript:doDrawing
doDrawing = function($chartDrawDiv, ds, obj) {
    require(['https://cdn.amcharts.com/lib/5/index.js','https://cdn.amcharts.com/lib/5/venn.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 container = root.container.children.push(am5.Container.new(root, {
              width: am5.p100,
              height: am5.p100,
              layout: root.verticalLayout
            }));
            
            var chart = container.children.push(am5venn.Venn.new(root, {
              categoryField: "name",
              valueField: "value",
              intersectionsField: "sets",
              paddingTop: 40,
              paddingBottom: 40,
              paddingLeft: 40,
              paddingRight: 40
            }));
            
            var pattern = am5.CirclePattern.new(root, {
              fill: am5.color(0x000000),
              color: am5.color(0xffffff),
              radius: 10,
              gap: 10,
              checkered: true
            })
            
            chart.slices.template.setAll({ templateField: "sliceSettings" });
            chart.labels.template.set("fill", am5.color(0xffffff));
            chart.labels.template.setup = function(target) {
              target.set("background", am5.RoundedRectangle.new(root, {
                stroke: am5.color(0xffffffff),
                fill: am5.color(0x000000),
                cornerRadiusTL: 5,
                cornerRadiusTR: 5,
                cornerRadiusBL: 5,
                cornerRadiusBR: 5,
                fillOpacity: 1
              }));
            };
            
            chart.data.setAll(ds);

            chart.hoverGraphics.setAll({
              strokeDasharray: [3, 3],
              stroke: am5.color(0xffffff),
              strokeWidth: 2
            });
        });
    });
};
```

* `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/)のテーマが指定可能です。
* `container`、`pattern`、`chart.slices`、`chart.labels`、`chart.hoverGraphics` では、チャートの見た目を定義しています。各項目の詳細は、[Common elements](https://www.amcharts.com/docs/v5/concepts/common-elements/) や [Venn diagram](https://www.amcharts.com/docs/v5/charts/venn/) をご参照ください
* `chart.data.setAll(ds)`の中で、作成したデータセットを、描画用のメソッドに受け渡しています。渡す際のデータ型は、JSON オブジェクトである必要があります

これでベン図の完成です。

## 最後に
Yellowfin と JS チャートを連携する際、Yellowfin のデータを、いかにしてチャートの仕様に合わせたデータ型に生成するががカギとなります。
今回のような、配列の入れ子になっているデータ型でも、さほど問題なく生成することができました。
Yellowfin の機能を補完することができるように、様々なチャートの作成に挑戦してみようと思います。

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