はじめに
amCharts と連携して、自動で軸と数値が入れ替わる棒グラフを作成したいと思います。時系列に応じて数値が変化する場合など、数値の変化の様子がとても分かりやすくなります。
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 をテーブルに配置します。
2-2. [グラフ] ステップ
[グラフ] ステップに進み、画面右側 [グラフの選択] から [JavaScriptグラフ] を選択します。
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 = 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 年のデータとしては、以下のような連装配列を生成する必要があります。
{
"Apple":2067000000000,
"Microsoft":1788000000000,
"Alphabet":1148000000000,
"Amazon":856940000000,
"Berkshire Hathaway":678720000000,
"Tesla":388970000000,
"NVIDIA":359500000000,
"Meta":315560000000
}
そのための処理が以下です。
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 から呼び出します。
#chartdiv{
height: 500px;
width: 700px;
}
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. 動作確認
チャートを表示してみると、2023 年末から 2025 年 4 月 16 日終値までの 8 社の株価の動きがアニメーションで確認できます。2024 年に各社の時価総額が大きく上振れするものの、2025 年 4 月に入ると、マグニフィセント 7 の時価総額が下落している様子が把握できます。トランプ関税の影響で、特に iPhone の生産の中心が中国である Apple 社の減少幅は大きいですね。NVIDIA も、近年は右肩上がりに時価総額を伸ばしてきているものの、トランプ関税の影響で、株価を低減させている様子がアニメーションで確認できます。
一方で、近年は日本の商社株を買い増ししていることでも知られるバークシャー・ハサウェイは、堅調に時価総額を伸ばしています。さすがウォーレン・バフェット、しごできな爺さんですね。
最後に
手動で切り替えながら数値の動きを見るのも良いのですが、アニメーションでしか表現できないものもありますよね。何より、動きのあるチャートは見栄えがします。
そんな時に利用をご検討ください。
では皆様、良いデータ分析を!