0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

amCharts と連携して、動きが面白いチャートを作成します。
アニメーション効果の棒グラフ Yellowfin と amCharts の連携では、自動で軸と数値が入れ替わっていく棒グラフを作成しました。第 2 弾は、マウスをホバーしたバーのアイコンが動く棒グラフを作成します。
bullet_high.gif

1. 事前準備

1-1. 参照データ

Worldometerから、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. ビューの作成まで

  • 以前の記事 を参考に、『1-2. ビューの作成』 までの作業を実施します。

2. グラフの作成

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

2-1. [データ] ステップ

  • country、flag、population をテーブルに配置します。
    image.png

2-2. [グラフ] ステップ

  • [グラフ] ステップに進み、画面右側 [グラフの選択] から [JavaScriptグラフ] を選択します。
    image.png
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
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
  • データとしては、以下のような連装配列を生成する必要があります。
ds
[
  {
    "name":"India",
    "steps":1463865525,
    "pictureSettings":{"src":"https://your_domain/india.png"}
  },
  {
    "name":"China",
    "steps":1416096094,
    "pictureSettings":{"src":"https://your_domain/china.png"}
  },

  ・・・

]
  • そのための処理が以下です。
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
#chartdiv{
    height: 500px;
    width: 700px;
}
doDrawing
  • チャートの描画に関わる処理を記述します。
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 ライブラリをダウンロードして利用することも可能です。
  • var root = am5.Root.new("chartdiv") で、CSS で定義した id を指定します。
  • 先の処理で生成したデータセットを、var data = ds でライブラリに受け渡しています。
  • それ以降の処理では、テーマチャートシリーズアニメーション、画像、色、ツールチップを設定した後、最後にチャートをロードしています。それぞれの設定箇所は、コメントアウト行をご確認下さい。

3. 動作確認

bullet_mid.gif

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

4. おまけ

  • コードを少し変更すると、縦向きのチャートに変身。
    image.png
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);
        });        
    })
 };

最後に

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?