アニメーション効果の棒グラフ 第2弾 ‐ Yellowfin と amCharts の連携

## はじめに
[amCharts](https://www.amcharts.com/) と連携して、動きが面白いチャートを作成します。
[アニメーション効果の棒グラフ Yellowfin と amCharts の連携](https://qiita.com/ExtremeCarvFJ/items/350e903ef3b782941ae0)では、自動で軸と数値が入れ替わっていく棒グラフを作成しました。第 ２ 弾は、マウスをホバーしたバーのアイコンが動く棒グラフを作成します。
![bullet_high.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/d89287e8-866e-4b91-9ad9-22cd340debcc.gif)

## 1. 事前準備
#### 1-1. 参照データ
[Worldometer](https://www.worldometers.info/)から、2025 年 3 月時点の世界各国地域の人口データを入手し、下記のようなテーブルを作成しました。総人口上位 5 か国 + 日本です。
|country|flag|population|
|---|---|---|
|India|https://your_domain/india.png|1463865525|
|China|https://your_domain/china.png|1416096094|
|USA|https://your_domain/usa.png|347275807|
|Indonesia|https://your_domain/indonesia.png|285721236|
|Pakistan|https://your_domain/pakistan.png|255219554|
|Japan|https://your_domain/japan.png|123103479|

#### 1-2. ビューの作成まで
* [以前の記事](https://qiita.com/ExtremeCarvFJ/items/ad453a6110cec093bdb9)  を参考に、『1-2. ビューの作成』 までの作業を実施します。

## 2. グラフの作成
* 準備が整ったら、グラフの作成に手順を進めます。新規でレポートを作成し、以下の手順に従ってグラフを作成します。

#### 2-1. [データ] ステップ
* country、flag、population をテーブルに配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/25338baf-39bc-4d30-ad8c-e9c35062cf80.png)

#### 2-2. [グラフ] ステップ
* [グラフ] ステップに進み、画面右側 [グラフの選択] から [JavaScriptグラフ] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/2b0b65a2-9073-4eb7-8239-396737287c47.png)

##### JavaScriptタブ
* 雛形を全て削除し、以下のコードに置き換えます。コードの処理の内容は後ほど説明します。
```javascript:全容
generateChart = function(options) {
    var $chartDrawDiv = $(options.divSelector);
    var processedData = processData(options.dataset.data);
    doDrawing($chartDrawDiv, processedData);
 },
 
 processData = function(dataset) {
     var src = [];
     var ds = [];
     for (i=0;i<dataset.country.length;i++){
         src.push({src:dataset.flag[i].raw_data})
             ds.push({
                 name: dataset.country[i].raw_data,
                 steps: dataset.population[i].raw_data,
                 pictureSettings: src[i],
             });
     }
     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/xy.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 = ds;
                
                //チャートの設定
                var chart = root.container.children.push(
                  am5xy.XYChart.new(root, {
                    panX: false,
                    panY: false,
                    paddingLeft:0,
                    paddingRight:30,
                    wheelX: "none",
                    wheelY: "none"
                  })
                );
                
                //軸の設定
                var yRenderer = am5xy.AxisRendererY.new(root, {
                  minorGridEnabled:true
                });
                yRenderer.grid.template.set("visible", false);
                
                var yAxis = chart.yAxes.push(
                  am5xy.CategoryAxis.new(root, {
                    categoryField: "name",
                    renderer: yRenderer,
                    paddingRight:40
                  })
                );
                
                var xRenderer = am5xy.AxisRendererX.new(root, {
                  minGridDistance:80,
                  minorGridEnabled:true
                });
                
                var xAxis = chart.xAxes.push(
                  am5xy.ValueAxis.new(root, {
                    min: 0,
                    renderer: xRenderer
                  })
                );
                
                // シリーズなどの設定
                var series = chart.series.push(
                  am5xy.ColumnSeries.new(root, {
                    name: "Income",
                    xAxis: xAxis,
                    yAxis: yAxis,
                    valueXField: "steps",
                    categoryYField: "name",
                    sequencedInterpolation: true,
                    calculateAggregates: true,
                    maskBullets: false,
                    tooltip: am5.Tooltip.new(root, {
                      dy: -30,
                      pointerOrientation: "horizontal",
                      labelText: "{valueX}"
                    })
                  })
                );
                
                series.columns.template.setAll({
                  strokeOpacity: 0,
                  cornerRadiusBR: 10,
                  cornerRadiusTR: 10,
                  cornerRadiusBL: 10,
                  cornerRadiusTL: 10,
                  maxHeight: 50,
                  fillOpacity: 0.8
                });
                
                //マウスをバーにホバーした時のブレットの動き
                var currentlyHovered;
                
                series.columns.template.events.on("pointerover", function(e) {
                  handleHover(e.target.dataItem);
                });
                
                series.columns.template.events.on("pointerout", function(e) {
                  handleOut();
                });
                
                function handleHover(dataItem) {
                  if (dataItem && currentlyHovered != dataItem) {
                    handleOut();
                    currentlyHovered = dataItem;
                    var bullet = dataItem.bullets[0];
                    bullet.animate({
                      key: "locationX",
                      to: 1,
                      duration: 600,
                      easing: am5.ease.out(am5.ease.cubic)
                    });
                  }
                }
                
                function handleOut() {
                  if (currentlyHovered) {
                    var bullet = currentlyHovered.bullets[0];
                    bullet.animate({
                      key: "locationX",
                      to: 0,
                      duration: 600,
                      easing: am5.ease.out(am5.ease.cubic)
                    });
                  }
                }
                
                //ブレットと画像の設定
                var circleTemplate = am5.Template.new({});
                
                series.bullets.push(function(root, series, dataItem) {
                  var bulletContainer = am5.Container.new(root, {});
                  var circle = bulletContainer.children.push(
                    am5.Circle.new(
                      root,
                      {
                        radius: 34
                      },
                      circleTemplate
                    )
                  );
                
                  var maskCircle = bulletContainer.children.push(
                    am5.Circle.new(root, { radius: 27 })
                  );
                
                  var imageContainer = bulletContainer.children.push(
                    am5.Container.new(root, {
                      mask: maskCircle
                    })
                  );
                
                  var image = imageContainer.children.push(
                    am5.Picture.new(root, {
                      templateField: "pictureSettings",
                      centerX: am5.p50,
                      centerY: am5.p50,
                      width: 60,
                      height: 60
                    })
                  );
                
                  return am5.Bullet.new(root, {
                    locationX: 0,
                    sprite: bulletContainer
                  });
                });
                
                // 数値と色の関係
                series.set("heatRules", [
                  {
                    dataField: "valueX",
                    min: am5.color(0xe5dc36),
                    max: am5.color(0x5faa46),
                    target: series.columns.template,
                    key: "fill"
                  },
                  {
                    dataField: "valueX",
                    min: am5.color(0xe5dc36),
                    max: am5.color(0x5faa46),
                    target: circleTemplate,
                    key: "fill"
                  }
                ]);
                
                series.data.setAll(data);
                yAxis.data.setAll(data);
                
                //ツールチップの動き
                var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
                cursor.lineX.set("visible", false);
                cursor.lineY.set("visible", false);
                
                cursor.events.on("cursormoved", function() {
                  var dataItem = series.get("tooltip").dataItem;
                  if (dataItem) {
                    handleHover(dataItem)
                  }
                  else {
                    handleOut();
                  }
                })
                
                // チャートのロード
                series.appear();
                chart.appear(1000, 100);
            });
        })
    };
```

## コード

簡単に処理内容を説明します。

##### 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
* データとしては、以下のような連装配列を生成する必要があります。
```JSON:ds
[
  {
    "name":"India",
    "steps":1463865525,
    "pictureSettings":{"src":"https://your_domain/india.png"}
  },
  {
    "name":"China",
    "steps":1416096094,
    "pictureSettings":{"src":"https://your_domain/china.png"}
  },

  ・・・

]
```

* そのための処理が以下です。
```JavaScript:processData
processData = function(dataset) {
     var src = [];
     var ds = [];
     for (i=0;i<dataset.country.length;i++){
         src.push({src:dataset.flag[i].raw_data})
             ds.push({
                 name: dataset.country[i].raw_data,
                 steps: dataset.population[i].raw_data,
                 pictureSettings: src[i],
             });
     }
     console.log(JSON.stringify(ds));
     return ds;
 },
```

* 生成されるデータの中身を確認したい場合は、`console.log(JSON.stringify(ds))` で、データをコンソールに出力しておくと便利です。

##### CSS
* CSS タブに下記を記述します。後ほど、定義した id を JavaScript から呼び出します。
```css:css
#chartdiv{
    height: 500px;
    width: 700px;
}
```

##### doDrawing
* チャートの描画に関わる処理を記述します。
```JavaScript:doDrawing
doDrawing = function($chartDrawDiv, ds, obj) {
     require(['https://cdn.amcharts.com/lib/5/index.js',
             'https://cdn.amcharts.com/lib/5/xy.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 = ds;
                
                //チャートの設定
                var chart = root.container.children.push(
                  am5xy.XYChart.new(root, {
                    panX: false,
                    panY: false,
                    paddingLeft:0,
                    paddingRight:30,
                    wheelX: "none",
                    wheelY: "none"
                  })
                );
                
                //軸の設定
                var yRenderer = am5xy.AxisRendererY.new(root, {
                  minorGridEnabled:true
                });
                yRenderer.grid.template.set("visible", false);
                
                var yAxis = chart.yAxes.push(
                  am5xy.CategoryAxis.new(root, {
                    categoryField: "name",
                    renderer: yRenderer,
                    paddingRight:40
                  })
                );
                
                var xRenderer = am5xy.AxisRendererX.new(root, {
                  minGridDistance:80,
                  minorGridEnabled:true
                });
                
                var xAxis = chart.xAxes.push(
                  am5xy.ValueAxis.new(root, {
                    min: 0,
                    renderer: xRenderer
                  })
                );
                
                // シリーズなどの設定
                var series = chart.series.push(
                  am5xy.ColumnSeries.new(root, {
                    name: "Income",
                    xAxis: xAxis,
                    yAxis: yAxis,
                    valueXField: "steps",
                    categoryYField: "name",
                    sequencedInterpolation: true,
                    calculateAggregates: true,
                    maskBullets: false,
                    tooltip: am5.Tooltip.new(root, {
                      dy: -30,
                      pointerOrientation: "horizontal",
                      labelText: "{valueX}"
                    })
                  })
                );
                
                series.columns.template.setAll({
                  strokeOpacity: 0,
                  cornerRadiusBR: 10,
                  cornerRadiusTR: 10,
                  cornerRadiusBL: 10,
                  cornerRadiusTL: 10,
                  maxHeight: 50,
                  fillOpacity: 0.8
                });
                
                //マウスをバーにホバーした時のブレットの動き
                var currentlyHovered;
                
                series.columns.template.events.on("pointerover", function(e) {
                  handleHover(e.target.dataItem);
                });
                
                series.columns.template.events.on("pointerout", function(e) {
                  handleOut();
                });
                
                function handleHover(dataItem) {
                  if (dataItem && currentlyHovered != dataItem) {
                    handleOut();
                    currentlyHovered = dataItem;
                    var bullet = dataItem.bullets[0];
                    bullet.animate({
                      key: "locationX",
                      to: 1,
                      duration: 600,
                      easing: am5.ease.out(am5.ease.cubic)
                    });
                  }
                }
                
                function handleOut() {
                  if (currentlyHovered) {
                    var bullet = currentlyHovered.bullets[0];
                    bullet.animate({
                      key: "locationX",
                      to: 0,
                      duration: 600,
                      easing: am5.ease.out(am5.ease.cubic)
                    });
                  }
                }
                
                //ブレットと画像の設定
                var circleTemplate = am5.Template.new({});
                
                series.bullets.push(function(root, series, dataItem) {
                  var bulletContainer = am5.Container.new(root, {});
                  var circle = bulletContainer.children.push(
                    am5.Circle.new(
                      root,
                      {
                        radius: 34
                      },
                      circleTemplate
                    )
                  );
                
                  var maskCircle = bulletContainer.children.push(
                    am5.Circle.new(root, { radius: 27 })
                  );
                
                  var imageContainer = bulletContainer.children.push(
                    am5.Container.new(root, {
                      mask: maskCircle
                    })
                  );
                
                  var image = imageContainer.children.push(
                    am5.Picture.new(root, {
                      templateField: "pictureSettings",
                      centerX: am5.p50,
                      centerY: am5.p50,
                      width: 60,
                      height: 60
                    })
                  );
                
                  return am5.Bullet.new(root, {
                    locationX: 0,
                    sprite: bulletContainer
                  });
                });
                
                // 数値と色の関係
                series.set("heatRules", [
                  {
                    dataField: "valueX",
                    min: am5.color(0xe5dc36),
                    max: am5.color(0x5faa46),
                    target: series.columns.template,
                    key: "fill"
                  },
                  {
                    dataField: "valueX",
                    min: am5.color(0xe5dc36),
                    max: am5.color(0x5faa46),
                    target: circleTemplate,
                    key: "fill"
                  }
                ]);
                
                series.data.setAll(data);
                yAxis.data.setAll(data);
                
                //ツールチップの動き
                var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
                cursor.lineX.set("visible", false);
                cursor.lineY.set("visible", false);
                
                cursor.events.on("cursormoved", function() {
                  var dataItem = series.get("tooltip").dataItem;
                  if (dataItem) {
                    handleHover(dataItem)
                  }
                  else {
                    handleOut();
                  }
                })
                
                // チャートのロード
                series.appear();
                chart.appear(1000, 100);
            });
        })
    };
```

* `require[]` の中で、JavaScriptライブラリの場所を指定します。上記の例では CDN のアドレスを指定していますが、JS ライブラリを[ダウンロード](https://www.amcharts.com/download/)して利用することも可能です。
* `var root = am5.Root.new("chartdiv")` で、CSS で定義した id を指定します。
* 先の処理で生成したデータセットを、`var data = ds` でライブラリに受け渡しています。
* それ以降の処理では、[テーマ](https://www.amcharts.com/docs/v5/concepts/themes/)、[チャート](https://www.amcharts.com/docs/v5/charts/xy-chart/
)、[軸](https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
)、[シリーズ](https://www.amcharts.com/docs/v5/charts/xy-chart/series/
)、[アニメーション](https://www.amcharts.com/docs/v5/concepts/animations/
)、画像、色、ツールチップを設定した後、最後にチャートをロードしています。それぞれの設定箇所は、コメントアウト行をご確認下さい。

## 3. 動作確認
![bullet_mid.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/afc3fe56-ce35-48f5-b838-0eb7aa627cc3.gif)

チャートを表示してみると、第 3 位の米国を大きく突き放し、インドと中国の人口の多さが目立ちます。
日本は国土が小さいため、他の 5 か国と比べて人口が少ないのは仕方のない部分もありますが、今後はさらに人口の減少が見込まれます。特に、生産労働人口の減少が顕著となる見込みです。果たして年金制度は維持できるんでしょうか。老後が心配、、、

## 4. おまけ
* コードを少し変更すると、縦向きのチャートに変身。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/4735ed23-68e1-4e3c-a9af-eefac584a82d.png)

```JavaScript:doDrawing
generateChart = function(options) {
    var $chartDrawDiv = $(options.divSelector);
    var processedData = processData(options.dataset.data);
    doDrawing($chartDrawDiv, processedData);
 },
 
 processData = function(dataset) {
     var src = [];
     var ds = [];
     for (i=0;i<dataset.country.length;i++){
         src.push({src:dataset.flag[i].raw_data})
             ds.push({
                 name: dataset.country[i].raw_data,
                 steps: dataset.population[i].raw_data,
                 pictureSettings: src[i],
             });
     }
     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/xy.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 = ds;
             
            // チャートの設定
            var chart = root.container.children.push(
               am5xy.XYChart.new(root, {
                 panX: false,
                 panY: false,
                 wheelX: "none",
                 wheelY: "none",
                 paddingBottom: 50,
                 paddingTop: 40,
                 paddingLeft:0,
                 paddingRight:0
               })
            );
             
            // 軸の設定
            var xRenderer = am5xy.AxisRendererX.new(root, {
               minorGridEnabled:true,
               minGridDistance:60
            });
            xRenderer.grid.template.set("visible", false);
             
            var xAxis = chart.xAxes.push(
               am5xy.CategoryAxis.new(root, {
                 paddingTop:40,
                 categoryField: "name",
                 renderer: xRenderer
               })
            );
             
             
            var yRenderer = am5xy.AxisRendererY.new(root, {});
            yRenderer.grid.template.set("strokeDasharray", [3]);
             
            var yAxis = chart.yAxes.push(
               am5xy.ValueAxis.new(root, {
                 min: 0,
                 renderer: yRenderer
               })
            );
             
            // シリーズの追加
            var series = chart.series.push(
               am5xy.ColumnSeries.new(root, {
                 name: "Income",
                 xAxis: xAxis,
                 yAxis: yAxis,
                 valueYField: "steps",
                 categoryXField: "name",
                 sequencedInterpolation: true,
                 calculateAggregates: true,
                 maskBullets: false,
                 tooltip: am5.Tooltip.new(root, {
                   dy: -30,
                   pointerOrientation: "vertical",
                   labelText: "{valueY}"
                 })
               })
            );
             
            series.columns.template.setAll({
               strokeOpacity: 0,
               cornerRadiusBR: 10,
               cornerRadiusTR: 10,
               cornerRadiusBL: 10,
               cornerRadiusTL: 10,
               maxWidth: 50,
               fillOpacity: 0.8
            });
             
            //マウスをバーにホバーした時のブレットの動き
            var currentlyHovered;
             
            series.columns.template.events.on("pointerover", function (e) {
               handleHover(e.target.dataItem);
            });
             
            series.columns.template.events.on("pointerout", function (e) {
               handleOut();
            });
             
            function handleHover(dataItem) {
               if (dataItem && currentlyHovered != dataItem) {
                 handleOut();
                 currentlyHovered = dataItem;
                 var bullet = dataItem.bullets[0];
                 bullet.animate({
                   key: "locationY",
                   to: 1,
                   duration: 600,
                   easing: am5.ease.out(am5.ease.cubic)
                 });
               }
            }
             
            function handleOut() {
               if (currentlyHovered) {
                 var bullet = currentlyHovered.bullets[0];
                 bullet.animate({
                   key: "locationY",
                   to: 0,
                   duration: 600,
                   easing: am5.ease.out(am5.ease.cubic)
                 });
               }
            }
             
            //ブレットと画像の設定
            var circleTemplate = am5.Template.new({});
             
            series.bullets.push(function (root, series, dataItem) {
               var bulletContainer = am5.Container.new(root, {});
               var circle = bulletContainer.children.push(
                 am5.Circle.new(
                   root,
                   {
                     radius: 34
                   },
                   circleTemplate
                 )
               );
             
               var maskCircle = bulletContainer.children.push(
                 am5.Circle.new(root, { radius: 27 })
               );
             
               var imageContainer = bulletContainer.children.push(
                 am5.Container.new(root, {
                   mask: maskCircle
                 })
               );
             
               var image = imageContainer.children.push(
                 am5.Picture.new(root, {
                   templateField: "pictureSettings",
                   centerX: am5.p50,
                   centerY: am5.p50,
                   width: 40,
                   height: 40
                 })
               );
             
               return am5.Bullet.new(root, {
                 locationY: 0,
                 sprite: bulletContainer
               });
            });
             
            // 数値と色の関係
            series.set("heatRules", [
               {
                 dataField: "valueY",
                 min: am5.color(0xe5dc36),
                 max: am5.color(0x5faa46),
                 target: series.columns.template,
                 key: "fill"
               },
               {
                 dataField: "valueY",
                 min: am5.color(0xe5dc36),
                 max: am5.color(0x5faa46),
                 target: circleTemplate,
                 key: "fill"
               }
            ]);
             
            series.data.setAll(data);
            xAxis.data.setAll(data);
             
            //ツールチップの動き
            var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
            cursor.lineX.set("visible", false);
            cursor.lineY.set("visible", false);
             
            cursor.events.on("cursormoved", function () {
               var dataItem = series.get("tooltip").dataItem;
               if (dataItem) {
                 handleHover(dataItem);
               } else {
                 handleOut();
               }
            });
             
            // チャートのロード
            series.appear();
            chart.appear(1000, 100);
        });        
    })
 };
```

## 最後に
動きが面白いこと以外、特に通常の棒グラフと用途は変わりませんが、こんなチャートがあっても良いのでは。

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