はじめに
amCharts と連携して、動きが面白いチャートを作成します。
amCharts には、時系列データを可視化する目的の Timeline チャートが備わっています。Timeline チャートをうまくカスタマイズして、トラックチャートに仕上げます。
1. 事前準備
1-1. 参照データ
営業担当が 6 名いる前提で、架空のデータを作成しました。file 列のアドレスは凡例のアイコンに使われる画像ファイルです。track 列がトラックの番号、value 列が営業成績です。
| name | file | track | value |
|---|---|---|---|
| 山田よし子 | https://<your_url>/1.svg | 1 | 760 |
| ハーゼンバーグ太郎 | https://<your_url>/2.svg | 2 | 390 |
| 杉本みのり | https://<your_url>/3.svg | 3 | 520 |
| 谷崎のび太 | https://<your_url>/4.svg | 4 | 690 |
| 北村ゆり | https://<your_url>/5.svg | 5 | 480 |
| 工藤ぽっこり | https://<your_url>/6.svg | 6 | 440 |
1-2. ビューの作成まで
以前の記事 を参考に、『1-2. ビューの作成』 までの作業を実施します。
2. グラフの作成
準備が整ったら、グラフの作成に手順を進めます。新規でレポートを作成し、以下の手順に従ってグラフを作成します。
2-1. [データ] ステップ
name、track、value、file をテーブルに配置します。

2-2. [グラフ] ステップ
[グラフ] ステップに進み、画面右側 [グラフの選択] から [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.name.length;i++){
ds.push({
name:dataset.name[i].raw_data,
file:dataset.file[i].raw_data,
track:dataset.track[i].raw_data,
value:dataset.value[i].raw_data
});
}
console.log(JSON.stringify(ds));
return ds;
},
doDrawing = function($chartDrawDiv, ds) {
require(['https://cdn.amcharts.com/lib/5/index.js',
'https://cdn.amcharts.com/lib/5/xy.js',
'https://cdn.amcharts.com/lib/5/timeline.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)
]);
// TimelineのCurveChartを利用
// マウストラックに対する動作を設定
var chart = root.container.children.push(am5timeline.CurveChart.new(root, {
wheelY: "zoomX"
}));
//Y軸(ランナーの名前にかかわる軸)
var yRenderer = am5timeline.AxisRendererCurveY.new(root, {
axisLength: 250,
minGridDistance: 10,
axisLocation: 0
})
//トラック上の名前
yRenderer.labels.template.setAll({
fontSize: 11,
layer: 30,
fill: am5.color(0xffffff),
fontWeight: "bold",
centerX: am5.p100
});
//トラックの線
yRenderer.grid.template.setAll({
stroke: am5.color(0xffffff),
strokeOpacity: 1,
layer: 30
})
//トラックの色
yRenderer.axisFills.template.setAll({
fill: am5.color(0xb84f49),
forceHidden: false,
fillOpacity: 1,
visible: true
});
// X軸(トラックの距離を示す軸)
var xRenderer = am5timeline.AxisRendererCurveX.new(root, {
yRenderer: yRenderer,
rotateLabels: true,
points: getPoints(),
minGridDistance: 200,
stroke: am5.color(0x00000),
strokeOpacity:0.1,
strokeWidth:20
});
//100メートルごとの線
xRenderer.grid.template.setAll({
stroke: am5.color(0xffffff),
strokeOpacity: 1,
layer: 30
})
//メートルのラベル
xRenderer.labels.template.setAll({
maxPosition: 0.98,
fontSize: 13,
layer: 30,
fill: am5.color(0xffffff),
fontWeight: "bold",
centerY: am5.p100
});
//name分のトラックを表示
var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
categoryField: "name",
renderer: yRenderer,
fillRule: function (dataItem) {
const axisFill = dataItem.get("axisFill");
if (axisFill) {
axisFill.set("visible", true);
}
}
}));
//トラックの距離を設定
var xAxis = chart.xAxes.push(am5xy.ValueAxis.new(root, {
renderer: xRenderer,
min: 0,
max: 1000,
strictMinMax: true
}));
//Series(データポイント)の設定
var series = chart.series.push(am5timeline.CurveColumnSeries.new(root, {
maskBullets: false,
xAxis: xAxis,
yAxis: yAxis,
valueXField: "value",
categoryYField: "name",
tooltip: am5.Tooltip.new(root, {
labelText: "{category}: {value}"
})
}))
series.columns.template.setAll({
fill: am5.color(0xffffff),
strokeOpacity: 0,
fillOpacity: 0.5,
layer: 30,
interactive: true
});
//マウスを凡例にホバーした時のトラック上の動作
series.columns.template.states.create("hover", {
fillOpacity: 1
});
//Bulletの描画
series.bullets.push(function (root, series, dataItem) {
return am5.Bullet.new(root, {
sprite: am5.Circle.new(root, {
radius: 5,
fillOpacity: 1,
fill: am5.color(0xffffff)
})
});
});
//データをセット
var data = ds;
yAxis.data.setAll(data);
series.data.setAll(data);
series.appear(3000, 100);
// 凡例の設定
var legend = chart.curveContainer.children.push(am5.Legend.new(root, {
centerX: am5.p50,
centerY: am5.p50,
useDefaultMarker: true,
nameField: "categoryY",
maxWidth: 350
}));
//凡例の表示と動作の設定
legend.valueLabels.template.setAll({
forceHidden: true
});
legend.markers.template.setup = function (marker) {
marker.events.on("dataitemchanged", function () {
var dataItem = marker.dataItem
var seriesDataItem = dataItem.dataContext;
var picture = marker.children.push(am5.Picture.new(root, {
width: 30,
height: 30,
src: seriesDataItem.dataContext.file
}));
marker.set("width", 30);
marker.set("height", 30);
dataItem.on("markerRectangle", function (rectangle) {
rectangle.set("forceHidden", true);
})
});
}
//マウスポインターをトラック上にホバーした時とホバーを外した時の動作
legend.itemContainers.template.events.on("pointerover", function (e) {
var seriesDataItem = e.target.dataItem.dataContext;
if (seriesDataItem) {
var graphics = seriesDataItem.get("graphics");
if (graphics) {
graphics.hover();
}
}
});
legend.itemContainers.template.events.on("pointerout", function (e) {
var seriesDataItem = e.target.dataItem.dataContext;
if (seriesDataItem) {
var graphics = seriesDataItem.get("graphics");
if (graphics) {
graphics.unhover();
}
}
});
//実際に設定して有効化
legend.data.setAll(series.dataItems);
//Timelineチャートがトラックを形成するように計算
function getPoints() {
var points = [];
var radius = 300;
points.push({ x: 0, y: -300 });
points.push({ x: 300, y: -300 });
for (var k = 1; k < 50; k++) {
var angle = -180 - k / 50 * 180;
points.push({ y: radius * am5.math.cos(angle), x: 300 + radius * am5.math.sin(angle) });
}
points.push({ x: -300, y: 300 });
for (var k2 = 1; k2 < 50; k2++) {
var angle2 = -k2 / 50 * 180;
points.push({ y: radius * am5.math.cos(angle2), x: -300 + radius * am5.math.sin(angle2) });
}
points.push({ x: 0, y: -300 });
return points;
}
});
})};
コード
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
データとしては、以下のような連装配列を生成する必要があります
[
{
"name":"山田よし子",
"file":"https://<your_url>/1.svg",
"track":1,
"value":760
},
{
"name":"ハーゼンバーグ太郎",
"file":"https://<your_url>/2.svg",
"track":2,
"value":390}
]
そのための処理が以下です
processData = function(dataset) {
var ds = [];
for (i=0;i<dataset.name.length;i++){
ds.push({
name:dataset.name[i].raw_data,
file:dataset.file[i].raw_data,
track:dataset.track[i].raw_data,
value:dataset.value[i].raw_data
});
}
console.log(JSON.stringify(ds));
return ds;
},
生成されるデータの中身を確認したい場合は、console.log(JSON.stringify(ds)) で、データをコンソールに出力しておくと便利です。
CSS
CSS タブに下記を記述します。後ほど、定義した id を JavaScript から呼び出します。
#chartdiv{
height: 500px;
width: 700px;
}
doDrawing
チャートの描画に関わる処理を記述します。
doDrawing = function($chartDrawDiv, ds) {
require(['https://cdn.amcharts.com/lib/5/index.js',
'https://cdn.amcharts.com/lib/5/xy.js',
'https://cdn.amcharts.com/lib/5/timeline.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)
]);
// TimelineのCurveChartを利用
// マウストラックに対する動作を設定
var chart = root.container.children.push(am5timeline.CurveChart.new(root, {
wheelY: "zoomX"
}));
//Y軸(ランナーの名前にかかわる軸)
var yRenderer = am5timeline.AxisRendererCurveY.new(root, {
axisLength: 250,
minGridDistance: 10,
axisLocation: 0
})
//トラック上の名前
yRenderer.labels.template.setAll({
fontSize: 11,
layer: 30,
fill: am5.color(0xffffff),
fontWeight: "bold",
centerX: am5.p100
});
//トラックの線
yRenderer.grid.template.setAll({
stroke: am5.color(0xffffff),
strokeOpacity: 1,
layer: 30
})
//トラックの色
yRenderer.axisFills.template.setAll({
fill: am5.color(0xb84f49),
forceHidden: false,
fillOpacity: 1,
visible: true
});
// X軸(トラックの距離を示す軸)
var xRenderer = am5timeline.AxisRendererCurveX.new(root, {
yRenderer: yRenderer,
rotateLabels: true,
points: getPoints(),
minGridDistance: 200,
stroke: am5.color(0x00000),
strokeOpacity:0.1,
strokeWidth:20
});
//100メートルごとの線
xRenderer.grid.template.setAll({
stroke: am5.color(0xffffff),
strokeOpacity: 1,
layer: 30
})
//メートルのラベル
xRenderer.labels.template.setAll({
maxPosition: 0.98,
fontSize: 13,
layer: 30,
fill: am5.color(0xffffff),
fontWeight: "bold",
centerY: am5.p100
});
//name分のトラックを表示
var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
categoryField: "name",
renderer: yRenderer,
fillRule: function (dataItem) {
const axisFill = dataItem.get("axisFill");
if (axisFill) {
axisFill.set("visible", true);
}
}
}));
//トラックの距離を設定
var xAxis = chart.xAxes.push(am5xy.ValueAxis.new(root, {
renderer: xRenderer,
min: 0,
max: 1000,
strictMinMax: true
}));
//Series(データポイント)の設定
var series = chart.series.push(am5timeline.CurveColumnSeries.new(root, {
maskBullets: false,
xAxis: xAxis,
yAxis: yAxis,
valueXField: "value",
categoryYField: "name",
tooltip: am5.Tooltip.new(root, {
labelText: "{category}: {value}"
})
}))
series.columns.template.setAll({
fill: am5.color(0xffffff),
strokeOpacity: 0,
fillOpacity: 0.5,
layer: 30,
interactive: true
});
//マウスを凡例にホバーした時のトラック上の動作
series.columns.template.states.create("hover", {
fillOpacity: 1
});
//Bulletの描画
series.bullets.push(function (root, series, dataItem) {
return am5.Bullet.new(root, {
sprite: am5.Circle.new(root, {
radius: 5,
fillOpacity: 1,
fill: am5.color(0xffffff)
})
});
});
//データをセット
var data = ds;
yAxis.data.setAll(data);
series.data.setAll(data);
series.appear(3000, 100);
// 凡例の設定
var legend = chart.curveContainer.children.push(am5.Legend.new(root, {
centerX: am5.p50,
centerY: am5.p50,
useDefaultMarker: true,
nameField: "categoryY",
maxWidth: 350
}));
//凡例の表示と動作の設定
legend.valueLabels.template.setAll({
forceHidden: true
});
legend.markers.template.setup = function (marker) {
marker.events.on("dataitemchanged", function () {
var dataItem = marker.dataItem
var seriesDataItem = dataItem.dataContext;
var picture = marker.children.push(am5.Picture.new(root, {
width: 30,
height: 30,
src: seriesDataItem.dataContext.file
}));
marker.set("width", 30);
marker.set("height", 30);
dataItem.on("markerRectangle", function (rectangle) {
rectangle.set("forceHidden", true);
})
});
}
//マウスポインターをトラック上にホバーした時とホバーを外した時の動作
legend.itemContainers.template.events.on("pointerover", function (e) {
var seriesDataItem = e.target.dataItem.dataContext;
if (seriesDataItem) {
var graphics = seriesDataItem.get("graphics");
if (graphics) {
graphics.hover();
}
}
});
legend.itemContainers.template.events.on("pointerout", function (e) {
var seriesDataItem = e.target.dataItem.dataContext;
if (seriesDataItem) {
var graphics = seriesDataItem.get("graphics");
if (graphics) {
graphics.unhover();
}
}
});
//実際に設定して有効化
legend.data.setAll(series.dataItems);
//Timelineチャートがトラックを形成するように計算
function getPoints() {
var points = [];
var radius = 300;
points.push({ x: 0, y: -300 });
points.push({ x: 300, y: -300 });
for (var k = 1; k < 50; k++) {
var angle = -180 - k / 50 * 180;
points.push({ y: radius * am5.math.cos(angle), x: 300 + radius * am5.math.sin(angle) });
}
points.push({ x: -300, y: 300 });
for (var k2 = 1; k2 < 50; k2++) {
var angle2 = -k2 / 50 * 180;
points.push({ y: radius * am5.math.cos(angle2), x: -300 + radius * am5.math.sin(angle2) });
}
points.push({ x: 0, y: -300 });
return points;
}
});
-
require[]の中で、JavaScriptライブラリの場所を指定します。上記の例では CDN のアドレスを指定していますが、JS ライブラリをダウンロードして利用することも可能です。 -
var root = am5.Root.new("chartdiv")で、CSS で定義した id を指定します。 - アニメーションの動きを取り入れるために、ルートのテーマを
am5themes_Animatedに設定しています。 - 今回、Timeline チャートを加工してトラックを成形しています。y 軸をランナーの並びに利用しており、そのための設定が
yRenderer、yAxisに対して施されています。 - 一方の x 軸は、トラックの距離に使用しています。そのための設定が
xRenderer、xAxisに対して施されています。各箇所の詳細は、コード内のコメントアウトをご覧ください。 -
Seriesはデータポイントのことで、トラック上のポイントの動きに関わります。 -
var data = dsで、processData で生成した JSON の連想配列を、チャートに受け渡します。 -
yAxis.data.setAll(data)とseries.data.setAll(data)で、軸と Series にデータをセットします。 -
Legend(凡例) をトラック内部に収め、アイコン画像を並べて表示し、ホバーの動きや、クリック時の動作を設定しています。 -
function getPoints()では、タイムチャートをトラックの形に成型するための計算が記述されています。
3. 動作確認
最後に
昭和の時代の営業部の様子といえば、壁に営業成績を貼り出して、営業どうしを競争させていたイメージが強いのは私だけでしょうか。ちなみに、その様子を Copilot に描いてもらったのがこちら。魁!!男塾風に描いていただきました。コンプライアンスが厳しい令和の時代にも、こんな職場はまだあったりするんでしょうか。

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



