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 と連携して、動きが面白いチャートを作成します。
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 をテーブルに配置します。
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 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
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":"山田よし子",
      "file":"https://<your_url>/1.svg",
      "track":1,
      "value":760
   },
   {
      "name":"ハーゼンバーグ太郎",
      "file":"https://<your_url>/2.svg",
      "track":2,
      "value":390}
]

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

processData
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 から呼び出します。

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

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

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 軸をランナーの並びに利用しており、そのための設定が yRendereryAxis に対して施されています。
  • 一方の x 軸は、トラックの距離に使用しています。そのための設定が xRendererxAxis に対して施されています。各箇所の詳細は、コード内のコメントアウトをご覧ください。
  • Series はデータポイントのことで、トラック上のポイントの動きに関わります。
  • var data = ds で、processData で生成した JSON の連想配列を、チャートに受け渡します。
  • yAxis.data.setAll(data) series.data.setAll(data) で、軸と Series にデータをセットします。
  • Legend (凡例) をトラック内部に収め、アイコン画像を並べて表示し、ホバーの動きや、クリック時の動作を設定しています。
  • function getPoints() では、タイムチャートをトラックの形に成型するための計算が記述されています。

3. 動作確認

  • チャートを表示してみると、まずは一斉にトラックを全ランナー (営業担当) が走り出し、現状の距離 (営業成績) が表示されます。現在は、山田よし子さんが先頭を走っています。
    a.gif

  • トラック上や判例にマウスをホバーすると、該当箇所が強調されます。
    b.gif

  • 凡例のクリックで、データの選択、非選択が可能です。
    c.gif

  • マウストラックを動かすと、トラックの表示距離が変わります。
    d.gif

最後に

昭和の時代の営業部の様子といえば、壁に営業成績を貼り出して、営業どうしを競争させていたイメージが強いのは私だけでしょうか。ちなみに、その様子を Copilot に描いてもらったのがこちら。魁!!男塾風に描いていただきました。コンプライアンスが厳しい令和の時代にも、こんな職場はまだあったりするんでしょうか。
image.png

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

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?