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?

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

Posted at

はじめに

amCharts と連携して、自動で軸と数値が入れ替わる棒グラフを作成したいと思います。時系列に応じて数値が変化する場合など、数値の変化の様子がとても分かりやすくなります。
m7.gif

1. 事前準備

1-1. 参照データ

マグニフィセント 7 (Alphabet, Amazon, Apple, Meta/Facebook, Microsoft, NVIDIA, Tesla) にバークシャー・ハサウェイ (代表:投資家ウォーレン・バフェット) を加えた、米国企業 8 社の過去 3 年間の株式時価総額の推移を見てみようと思います。

year company price
2022 Apple 2,067 billion
・・・ ・・・ ・・・
2025 Tesla 776 billion

データはこちらから入手しました。 2022 年から 2024 年分は 12 月末時点のデータ、2025 年分は 4 月 16 日の終値になります。

1-2. ビューの作成まで

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

2. グラフの作成

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

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

year、company、price をテーブルに配置します。
image.png

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

[グラフ] ステップに進み、画面右側 [グラフの選択] から [JavaScriptグラフ] を選択します。
image.png

JavaScriptタブ

雛形を全て削除し、以下のコードに置き換えます。コードの処理の内容は後ほど説明します。

全容
generateChart = function(options) {
    var $chartDrawDiv = $(options.divSelector);
    var processedData2022 = processData2022(options.dataset.data);
    var processedData2023 = processData2023(options.dataset.data);
    var processedData2024 = processData2024(options.dataset.data);
    var processedData2025 = processData2025(options.dataset.data);
    doDrawing($chartDrawDiv, processedData2022, processedData2023, processedData2024, processedData2025);
},

processData2022 = function(dataset) {
    var ds2022={};
    for (i=0;i<dataset.year.length;i++){
        if(dataset.year[i].raw_data === '2022'){
            key = dataset.company[i].raw_data;
            ds2022[key]= dataset.price[i].raw_data;
        }
    }
    console.log(JSON.stringify(ds2022));
    return ds2022;
},

processData2023 = function(dataset) {
    var ds2023={};
    for (k=0;k<dataset.year.length;k++){
        if(dataset.year[k].raw_data === '2023'){
            key = dataset.company[k].raw_data;
            ds2023[key]= dataset.price[k].raw_data;
        }
    }
    console.log(JSON.stringify(ds2023));
    return ds2023;
},

processData2024 = function(dataset) {
    var ds2024={};
    for (l=0;l<dataset.year.length;l++){
        if(dataset.year[l].raw_data === '2024'){
            key = dataset.company[l].raw_data;
            ds2024[key]= dataset.price[l].raw_data;
        }
    }
    console.log(JSON.stringify(ds2024));
    return ds2024;
},

processData2025 = function(dataset) {
    var ds2025={};
    for (m=0;m<dataset.year.length;m++){
        if(trim(dataset.year[m].raw_data) === '2025'){
            key = dataset.company[m].raw_data;
            ds2025[key]= dataset.price[m].raw_data;
        }
    }
    console.log(JSON.stringify(ds2025));
    return ds2025;
},

doDrawing = function($chartDrawDiv, ds2022, ds2023, ds2024, ds2025) {
   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(data) {

           // データセット受渡
            var allData = {
                "2022":ds2022,
                "2023":ds2023,
                "2024":ds2024,
                "2025":ds2025
            };
           
           // ルートエレメントの作成
           var root = am5.Root.new("chartdiv");
           
           root.numberFormatter.setAll({
             numberFormat: "#a",
           
             // M(millions 百万) B(billions 十億) 単位で丸め
             bigNumberPrefixes: [
               { number: 1e6, suffix: "M" },
               { number: 1e9, suffix: "B" }
             ],
           
             smallNumberPrefixes: []
           });
           
           var stepDuration = 2000;
           
           // テーマの設定
           root.setThemes([am5themes_Animated.new(root)]);
           
           // チャートの作成
           var chart = root.container.children.push(am5xy.XYChart.new(root, {
             panX: true,
             panY: true,
             wheelX: "none",
             wheelY: "none",
             paddingLeft: 0
           }));
           
           // ズームボタン非表示
           chart.zoomOutButton.set("forceHidden", true);
           
           // 軸の作成
           var yRenderer = am5xy.AxisRendererY.new(root, {
             minGridDistance: 20,
             inversed: true,
             minorGridEnabled: true
           });
           
           // グリッド非表示
           yRenderer.grid.template.set("visible", false);
           
           var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
             maxDeviation: 0,
             categoryField: "network",
             renderer: yRenderer
           }));
           
           var xAxis = chart.xAxes.push(am5xy.ValueAxis.new(root, {
             maxDeviation: 0,
             min: 0,
             strictMinMax: true,
             extraMax: 0.1,
             renderer: am5xy.AxisRendererX.new(root, {})
           }));
           
           xAxis.set("interpolationDuration", stepDuration / 10);
           xAxis.set("interpolationEasing", am5.ease.linear);
           
           // シリーズの追加
           var series = chart.series.push(am5xy.ColumnSeries.new(root, {
             xAxis: xAxis,
             yAxis: yAxis,
             valueXField: "value",
             categoryYField: "network"
           }));
           
           // 棒の角を丸く
           series.columns.template.setAll({ cornerRadiusBR: 5, cornerRadiusTR: 5 });
           
           // 棒の色を自動で選択
           series.columns.template.adapters.add("fill", function (fill, target) {
             return chart.get("colors").getIndex(series.columns.indexOf(target));
           });
           
           series.columns.template.adapters.add("stroke", function (stroke, target) {
             return chart.get("colors").getIndex(series.columns.indexOf(target));
           });
           
           // ラベルの設定
           series.bullets.push(function () {
             return am5.Bullet.new(root, {
               locationX: 1,
               sprite: am5.Label.new(root, {
                 text: "{valueXWorking.formatNumber('#.# a')}",
                 fill: root.interfaceColors.get("alternativeText"),
                 centerX: am5.p100,
                 centerY: am5.p50,
                 populateText: true
               })
             });
           });
           
           var label = chart.plotContainer.children.push(am5.Label.new(root, {
             text: "2022",
             fontSize: "8em",
             opacity: 0.2,
             x: am5.p100,
             y: am5.p100,
             centerY: am5.p100,
             centerX: am5.p100
           }));
           
           // カテゴリ毎にシリーズアイテムを取得
           function getSeriesItem(category) {
             for (var i = 0; i < series.dataItems.length; i++) {
               var dataItem = series.dataItems[i];
               if (dataItem.get("categoryY") == category) {
                 return dataItem;
               }
             }
           }
           
           // 軸の並び順
           function sortCategoryAxis() {
             // 数値の降順で並び替え
             series.dataItems.sort(function (x, y) {
               return y.get("valueX") - x.get("valueX");
             });
           
             // 軸アイテム毎に処理
             am5.array.each(yAxis.dataItems, function (dataItem) {
               // シリーズアイテムを取得
               var seriesDataItem = getSeriesItem(dataItem.get("category"));
           
               if (seriesDataItem) {
                 // シリーズデータアイテムのインデックスを取得
                 var index = series.dataItems.indexOf(seriesDataItem);
                 // アイテムのポジションを算出
                 var deltaPosition =
                   (index - dataItem.get("index", 0)) / series.dataItems.length;
                 // インデックスとシリーズデータアイテムのインデックスが同じになるように設定
                 if (dataItem.get("index") != index) {
                   dataItem.set("index", index);
                   // 各アイテムのポジションを設定(deltaPosition)
                   dataItem.set("deltaPosition", -deltaPosition);
                   // アニメーションのポジション(deltaPosition)を0に設定
                   dataItem.animate({
                     key: "deltaPosition",
                     to: 0,
                     duration: stepDuration / 2,
                     easing: am5.ease.out(am5.ease.cubic)
                   });
                 }
               }
             });
             // 軸のアイテムをインデックスに応じて設定
             yAxis.dataItems.sort(function (x, y) {
               return x.get("index") - y.get("index");
             });
           }
           
           var year = 2022;
           
           // 1.5秒毎にデータ更新
           var interval = setInterval(function () {
             year++;
           
             if (year > 2025) {
               clearInterval(interval);
               clearInterval(sortInterval);
             }
           
             updateData();
           }, stepDuration);
           
           var sortInterval = setInterval(function () {
             sortCategoryAxis();
           }, 100);
           
           function setInitialData() {
             var d = allData[year];
           
             for (var n in d) {
               series.data.push({ network: n, value: d[n] });
               yAxis.data.push({ network: n });
             }
           }
           
           function updateData() {
             var itemsWithNonZero = 0;
           
             if (allData[year]) {
               label.set("text", year.toString());
           
               am5.array.each(series.dataItems, function (dataItem) {
                 var category = dataItem.get("categoryY");
                 var value = allData[year][category];
           
                 if (value > 0) {
                   itemsWithNonZero++;
                 }
           
                 dataItem.animate({
                   key: "valueX",
                   to: value,
                   duration: stepDuration,
                   easing: am5.ease.linear
                 });
                 dataItem.animate({
                   key: "valueXWorking",
                   to: value,
                   duration: stepDuration,
                   easing: am5.ease.linear
                 });
               });
           
               yAxis.zoom(0, itemsWithNonZero / yAxis.dataItems.length);
             }
           }
           
           setInitialData();
           setTimeout(function () {
             year++;
             updateData();
           }, 50);
           
           // チャートのロード
           series.appear(1000);
           chart.appear(1000, 100);
           
        });
   });
};

コード

generateChart
generateChart
generateChart = function(options) {
    var $chartDrawDiv = $(options.divSelector);
    var processedData2022 = processData2022(options.dataset.data);
    var processedData2023 = processData2023(options.dataset.data);
    var processedData2024 = processData2024(options.dataset.data);
    var processedData2025 = processData2025(options.dataset.data);
    doDrawing($chartDrawDiv, processedData2022, processedData2023, processedData2024, processedData2025);
},

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

processData2022

2022 年のデータとしては、以下のような連装配列を生成する必要があります。

2022 年末時価総額
{
"Apple":2067000000000,
"Microsoft":1788000000000,
"Alphabet":1148000000000,
"Amazon":856940000000,
"Berkshire Hathaway":678720000000,
"Tesla":388970000000,
"NVIDIA":359500000000,
"Meta":315560000000
}

そのための処理が以下です。

processData2022
processData2022 = function(dataset) {
    var ds2022={};
    for (i=0;i<dataset.year.length;i++){
        if(dataset.year[i].raw_data === '2022'){
            key = dataset.company[i].raw_data;
            ds2022[key]= dataset.price[i].raw_data;
        }
    }
    console.log(JSON.stringify(ds2022));
    return ds2022;
},

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

同様に、processData2023、processData2024、processData2025 で、それぞれの年のデータを、各データセットに格納します。

CSS

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

css
#chartdiv{
    height: 500px;
    width: 700px;
}
doDrawing

チャートの描画に関わる処理を記述します。

doDrawing
doDrawing = function($chartDrawDiv, ds2022, ds2023, ds2024, ds2025) {
   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(data) {

           // データセット受渡
            var allData = {
                "2022":ds2022,
                "2023":ds2023,
                "2024":ds2024,
                "2025":ds2025
            };
           
           // ルートエレメントの作成
           var root = am5.Root.new("chartdiv");
           
           root.numberFormatter.setAll({
             numberFormat: "#a",
           
             // M(millions 百万) B(billions 十億) 単位で丸め
             bigNumberPrefixes: [
               { number: 1e6, suffix: "M" },
               { number: 1e9, suffix: "B" }
             ],
           
             smallNumberPrefixes: []
           });
           
           var stepDuration = 2000;
           
           // テーマの設定
           root.setThemes([am5themes_Animated.new(root)]);
           
           // チャートの作成
           var chart = root.container.children.push(am5xy.XYChart.new(root, {
             panX: true,
             panY: true,
             wheelX: "none",
             wheelY: "none",
             paddingLeft: 0
           }));
           
           // ズームボタン非表示
           chart.zoomOutButton.set("forceHidden", true);
           
           // 軸の作成
           var yRenderer = am5xy.AxisRendererY.new(root, {
             minGridDistance: 20,
             inversed: true,
             minorGridEnabled: true
           });
           
           // グリッド非表示
           yRenderer.grid.template.set("visible", false);
           
           var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
             maxDeviation: 0,
             categoryField: "network",
             renderer: yRenderer
           }));
           
           var xAxis = chart.xAxes.push(am5xy.ValueAxis.new(root, {
             maxDeviation: 0,
             min: 0,
             strictMinMax: true,
             extraMax: 0.1,
             renderer: am5xy.AxisRendererX.new(root, {})
           }));
           
           xAxis.set("interpolationDuration", stepDuration / 10);
           xAxis.set("interpolationEasing", am5.ease.linear);
           
           // シリーズの追加
           var series = chart.series.push(am5xy.ColumnSeries.new(root, {
             xAxis: xAxis,
             yAxis: yAxis,
             valueXField: "value",
             categoryYField: "network"
           }));
           
           // 棒の角を丸く
           series.columns.template.setAll({ cornerRadiusBR: 5, cornerRadiusTR: 5 });
           
           // 棒の色を自動で選択
           series.columns.template.adapters.add("fill", function (fill, target) {
             return chart.get("colors").getIndex(series.columns.indexOf(target));
           });
           
           series.columns.template.adapters.add("stroke", function (stroke, target) {
             return chart.get("colors").getIndex(series.columns.indexOf(target));
           });
           
           // ラベルの設定
           series.bullets.push(function () {
             return am5.Bullet.new(root, {
               locationX: 1,
               sprite: am5.Label.new(root, {
                 text: "{valueXWorking.formatNumber('#.# a')}",
                 fill: root.interfaceColors.get("alternativeText"),
                 centerX: am5.p100,
                 centerY: am5.p50,
                 populateText: true
               })
             });
           });
           
           var label = chart.plotContainer.children.push(am5.Label.new(root, {
             text: "2022",
             fontSize: "8em",
             opacity: 0.2,
             x: am5.p100,
             y: am5.p100,
             centerY: am5.p100,
             centerX: am5.p100
           }));
           
           // カテゴリ毎にシリーズアイテムを取得
           function getSeriesItem(category) {
             for (var i = 0; i < series.dataItems.length; i++) {
               var dataItem = series.dataItems[i];
               if (dataItem.get("categoryY") == category) {
                 return dataItem;
               }
             }
           }
           
           // 軸の並び順
           function sortCategoryAxis() {
             // 数値の降順で並び替え
             series.dataItems.sort(function (x, y) {
               return y.get("valueX") - x.get("valueX");
             });
           
             // 軸アイテム毎に処理
             am5.array.each(yAxis.dataItems, function (dataItem) {
               // シリーズアイテムを取得
               var seriesDataItem = getSeriesItem(dataItem.get("category"));
           
               if (seriesDataItem) {
                 // シリーズデータアイテムのインデックスを取得
                 var index = series.dataItems.indexOf(seriesDataItem);
                 // アイテムのポジションを算出
                 var deltaPosition =
                   (index - dataItem.get("index", 0)) / series.dataItems.length;
                 // インデックスとシリーズデータアイテムのインデックスが同じになるように設定
                 if (dataItem.get("index") != index) {
                   dataItem.set("index", index);
                   // 各アイテムのポジションを設定(deltaPosition)
                   dataItem.set("deltaPosition", -deltaPosition);
                   // アニメーションのポジション(deltaPosition)を0に設定
                   dataItem.animate({
                     key: "deltaPosition",
                     to: 0,
                     duration: stepDuration / 2,
                     easing: am5.ease.out(am5.ease.cubic)
                   });
                 }
               }
             });
             // 軸のアイテムをインデックスに応じて設定
             yAxis.dataItems.sort(function (x, y) {
               return x.get("index") - y.get("index");
             });
           }
           
           var year = 2022;
           
           // 1.5秒毎にデータ更新
           var interval = setInterval(function () {
             year++;
           
             if (year > 2025) {
               clearInterval(interval);
               clearInterval(sortInterval);
             }
           
             updateData();
           }, stepDuration);
           
           var sortInterval = setInterval(function () {
             sortCategoryAxis();
           }, 100);
           
           function setInitialData() {
             var d = allData[year];
           
             for (var n in d) {
               series.data.push({ network: n, value: d[n] });
               yAxis.data.push({ network: n });
             }
           }
           
           function updateData() {
             var itemsWithNonZero = 0;
           
             if (allData[year]) {
               label.set("text", year.toString());
           
               am5.array.each(series.dataItems, function (dataItem) {
                 var category = dataItem.get("categoryY");
                 var value = allData[year][category];
           
                 if (value > 0) {
                   itemsWithNonZero++;
                 }
           
                 dataItem.animate({
                   key: "valueX",
                   to: value,
                   duration: stepDuration,
                   easing: am5.ease.linear
                 });
                 dataItem.animate({
                   key: "valueXWorking",
                   to: value,
                   duration: stepDuration,
                   easing: am5.ease.linear
                 });
               });
           
               yAxis.zoom(0, itemsWithNonZero / yAxis.dataItems.length);
             }
           }
           
           setInitialData();
           setTimeout(function () {
             year++;
             updateData();
           }, 50);
           
           // チャートのロード
           series.appear(1000);
           chart.appear(1000, 100);
           
        });
   });
};
  • require[] の中で、JavaScriptライブラリの場所を指定します。上記の例では CDN のアドレスを指定していますが、JS ライブラリをダウンロードして利用することも可能です
  • var allData = {} の中で、各年のデータセットを配置しています
  • var root = am5.Root.new("chartdiv") で、CSS で定義した id を指定します
  • root.setThemes で、動きのある Animated テーマを設定しています
  • それ以降の処理の中では、チャートの表示を設定し、アニメーションの動きを定義しています。各箇所での設定は、コードの中のコメントアウト行を参考にしてください
  • 設定の詳細に関しては、XY チャートのドキュメントをご参照ください。シリーズに関するドキュメントもそろっています

3. 動作確認

m7.gif

チャートを表示してみると、2023 年末から 2025 年 4 月 16 日終値までの 8 社の株価の動きがアニメーションで確認できます。2024 年に各社の時価総額が大きく上振れするものの、2025 年 4 月に入ると、マグニフィセント 7 の時価総額が下落している様子が把握できます。トランプ関税の影響で、特に iPhone の生産の中心が中国である Apple 社の減少幅は大きいですね。NVIDIA も、近年は右肩上がりに時価総額を伸ばしてきているものの、トランプ関税の影響で、株価を低減させている様子がアニメーションで確認できます。
一方で、近年は日本の商社株を買い増ししていることでも知られるバークシャー・ハサウェイは、堅調に時価総額を伸ばしています。さすがウォーレン・バフェット、しごできな爺さんですね。

最後に

手動で切り替えながら数値の動きを見るのも良いのですが、アニメーションでしか表現できないものもありますよね。何より、動きのあるチャートは見栄えがします。
そんな時に利用をご検討ください。

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

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?