Apache ECharts を使って、kintone に統計ダッシュボードを表示しよう

目次

はじめに

データ分析をするときに、グラフやチャートなどを利用してデータを可視化する場面は多いと思います。
kintone の基本機能でもグラフは作成できますが、データ可視化ライブラリを使うことでより表現力の高いグラフを作成できます。

今回の記事では、オープンソースのデータ可視化ライブラリの Apache ECharts (External link) を使って、統計ダッシュボードを kintone に表示するカスタマイズを紹介します。

完成イメージ

今回のカスタマイズを適用することで、以下の画面に Apache ECharts で生成したグラフを表示させることができます。

  • レコード詳細画面
  • レコード一覧画面
  • ポータル画面
  • モバイル画面

レコード詳細画面にグラフを表示

レコードへ登録された数値を使って円グラフを生成し、該当レコードの詳細画面に表示します。

レコード一覧画面にグラフを表示

アプリの全レコードを取得し、取得した数値を元に積み上げグラフを生成し、レコード一覧画面に表示します。

ポータル画面にグラフを表示

該当アプリの全レコードを取得し、取得した数値を元に折れ線グラフを生成し、ポータル画面に表示します。
折れ線グラフの各日付の数値をマウスオーバーする際に、該当日付の数値の内訳を円グラフに表示します。

モバイル表示に対応

モバイルからも手軽にグラフを確認できます。

アプリの準備

グラフを表示するための「売上管理アプリ」を作成します。
「はじめから作成」でアプリを新規作成し、次のフィールドをアプリに追加してください。

フィールドの種類 フィールドコード 備考
スペース report 生成したグラフを表示するためのスペースです。
文字列(1 行) year グラフの「年度」情報を入力します。
日付 date グラフの「日付」情報を入力します。
文字列(1 行) department グラフの「製品」情報を入力します。
計算 total グラフの「総売上」情報を計算します。
計算式には、channel1+channel2+channel3+channel4+channel5 と設定します。
数値 channel1 〜 channel5 グラフの各地域の売上を入力します。
フィールドコードが channel1 〜 channel5 の数値フィールドを 5 つ追加します。

サンプルコード

echarts_app.js

レコードの詳細画面と一覧画面にグラフを表示するプログラムです。
JavaScript ファイルとして保存してください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
/*
 * echarts sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/

(() => {
  'use strict';
  // レコード詳細、レコード編集イベントで、該当レコードの売上数値を円グラフに表示
  kintone.events.on(['app.record.detail.show', 'app.record.edit.show'], (event) => {
    const pcSetting = {
      type: 'pc',
      showContent: true,
      style: 'width: 600px;height:400px;',
    };
    generateDetail(pcSetting, event);
    return event;
  });

  // モバイルのレコード詳細、レコード編集イベントで、該当レコードの売上数値を円グラフに表示
  kintone.events.on(['mobile.app.record.detail.show', 'mobile.app.record.edit.show'], (event) => {
    const mobileSetting = {
      type: 'mobile',
      showContent: false,
      style: 'width: 350px;height:400px;',
    };
    generateDetail(mobileSetting, event);
    return event;
  });

  // レコード一覧イベントで、該当アプリの全レコードの売上数値を取得し、積み上げグラフに表示
  kintone.events.on(['app.record.index.show'], (event) => {
    const pcSetting = {
      type: 'pc',
      showContent: true,
      style: 'width: 900px;height:400px;'
    };
    generateTotal(pcSetting);
    return event;
  });

  // モバイルのレコード一覧イベントで、該当アプリの全レコードの売上数値を取得し、積み上げグラフに表示
  kintone.events.on(['mobile.app.record.index.show'], (event) => {
    const mobileSetting = {
      type: 'mobile',
      showContent: false,
      style: 'width: 350px;height:400px;'
    };
    generateTotal(mobileSetting);
    return event;
  });

  // 該当レコードの売上数値を円グラフに表示する処理
  const generateDetail = (setting, event) => {
    const record = event.record;
    let report_el;
    // モバイル/PCの場合の、グラフを表示するスペースフィールドを取得
    if (setting.type === 'mobile') {
      report_el = kintone.mobile.app.record.getSpaceElement('report');
    } else {
      report_el = kintone.app.record.getSpaceElement('report');
    }
    // スペースフィールドに、円グラフを生成
    const report_div = document.createElement('div');
    report_div.id = 'graph';
    report_div.style = setting.style;
    const myChart = echarts.init(report_div);
    // 円グラフの詳細を指定
    const option = {
      title: {
        text: '地域別売上',
        x: 'center'
      },
      tooltip: {
        trigger: 'item',
        formatter: '{a} <br/>{b} : {c} ({d}%)',
        showContent: setting.showContent
      },
      legend: {
        orient: 'vertical',
        left: 'left',
        data: ['東京', '千葉', '埼玉', '神奈川', '栃木']
      },
      series: [
        {
          name: 'タイプ',
          type: 'pie',
          radius: '55%',
          center: ['50%', '60%'],
          data: [
            {value: record.channel1.value, name: '東京'},
            {value: record.channel2.value, name: '千葉'},
            {value: record.channel3.value, name: '埼玉'},
            {value: record.channel4.value, name: '神奈川'},
            {value: record.channel5.value, name: '栃木'}
          ],
          itemStyle: {
            emphasis: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: 'rgba(0, 0, 0, 0.5)'
            }
          }
        }
      ]
    };
    myChart.setOption(option);
    report_el.appendChild(report_div);
  };

  // 該当アプリの全レコードの売上数値を積み上げグラフに表示する処理
  const generateTotal = (setting) => {
    if (document.getElementById('graph') !== null) {
      return;
    }
    // モバイル/PCの場合の、レコード一覧のメニューの下側の空白部分の要素を取得し、その要素にグラフを表示
    const graph = document.createElement('div');
    graph.id = 'graph';
    graph.style = setting.style;
    let app_id;
    if (setting.type === 'mobile') {
      kintone.mobile.app.getHeaderSpaceElement().appendChild(graph);
      app_id = kintone.mobile.app.getId();
    } else {
      kintone.app.getHeaderSpaceElement().appendChild(graph);
      app_id = kintone.app.getId();
    }
    const myChart = echarts.init(graph);
    // KintoneRestAPIClient で該当アプリの全レコードを取得
    const kintoneRecord = new KintoneRestAPIClient();
    const rcOption = {
      app: app_id,
      query: 'order by date asc'
    };
    kintoneRecord.record.getAllRecords(rcOption).then((res) => {
      // グラフに表示するために、取得したレコードのデータを整形
      const graphData = {channel1: [], channel2: [], channel3: [], channel4: [], channel5: []};
      const dateArray = [];
      for (let j = 0; j < res.length; j++) {
        const record = res[j];
        const dateKey = record.date.value;
        graphData.channel1.push(record.channel1.value);
        graphData.channel2.push(record.channel2.value);
        graphData.channel3.push(record.channel3.value);
        graphData.channel4.push(record.channel4.value);
        graphData.channel5.push(record.channel5.value);
        dateArray.push(dateKey);
      }
      // グラフの詳細を指定し、取得した売上数値をグラフの data に指定
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          },
          showContent: setting.showContent
        },
        legend: {
          data: ['東京', '千葉', '埼玉', '神奈川', '栃木']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'value'
        },
        yAxis: {
          type: 'category',
          data: dateArray
        },
        series: [
          {
            name: '東京',
            type: 'bar',
            stack: '合計',
            label: {
              normal: {
                show: true,
                position: 'insideRight'
              }
            },
            data: graphData.channel1
          },
          {
            name: '千葉',
            type: 'bar',
            stack: '合計',
            label: {
              normal: {
                show: true,
                position: 'insideRight'
              }
            },
            data: graphData.channel2
          },
          {
            name: '埼玉',
            type: 'bar',
            stack: '合計',
            label: {
              normal: {
                show: true,
                position: 'insideRight'
              }
            },
            data: graphData.channel3
          },
          {
            name: '神奈川',
            type: 'bar',
            stack: '合計',
            label: {
              normal: {
                show: true,
                position: 'insideRight'
              }
            },
            data: graphData.channel4
          },
          {
            name: '栃木',
            type: 'bar',
            stack: '合計',
            label: {
              normal: {
                show: true,
                position: 'insideRight'
              }
            },
            data: graphData.channel5
          }
        ]
      };
      myChart.setOption(option);
    }).catch((err) => {
      // 該当アプリのレコード取得が失敗したときに、エラーを表示
      document.getElementById('graph').innerText = 'データの取得に失敗しました';
    });
  };
})();

echarts_portal.js

ポータル画面にグラフを表示するプログラムです。
69 行の「app_id」の値をご利用のアプリの id に変更したうえで、JavaScript ファイルとして保存してください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/*
 * echarts sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/

(() => {
  'use strict';
  // ポータル表示イベントで、該当アプリの全レコードの売上数値を取得し、数値を折れ線、円グラフに表示
  kintone.events.on('portal.show', (event) => {
    // グラフの詳細を指定
    const pcSetting = {
      type: 'pc',
      legend: {top: '10%'},
      grid: {left: '10%', width: '35%', top: '25%'},
      pie: {center: ['70%', '60%'], radius: '50%'},
      style: 'width: auto;height:400px;background-color:#fff;margin: 16px 16px 0 16px;'
    };
    // グラフを生成
    init(pcSetting);
    generateGraph(pcSetting);
    return event;
  });

  // モバイルのポータル表示イベントで、該当アプリの全レコードの売上数値を取得し、数値を折れ線、円グラフに表示
  kintone.events.on('mobile.portal.show', (event) => {
    // グラフの詳細を指定
    const mobileSetting = {
      type: 'mobile',
      legend: {top: '5%'},
      grid: {left: '10%', height: '40%', top: '18%'},
      pie: {center: ['50%', '80%'], radius: '30%'},
      style: 'width: auto;height:600px;background-color:#fff;margin: 10px 10px 0 10px;border-radius: 6px'
    };
    // グラフを生成
    init(mobileSetting);
    generateGraph(mobileSetting);
    return event;
  });

  // 該当アプリの全レコードの売上数値をグラフに表示する処理
  const generateGraph = (setting) => {
    const myChart = echarts.init(document.getElementById('graph'));
    // 表示される折れ線グラフの AxisPointer が変わるタイミングで、円グラフに新しい数値をセットする処理
    // 今回の例では、折れ線グラフの各日付のポインターをマウスオーバーする際に、該当日付の売上数値の内訳を円グラフに表示
    myChart.on('updateAxisPointer', (event) => {
      const xAxisInfo = event.axesInfo[0];
      if (xAxisInfo) {
        const dimension = xAxisInfo.value + 1;
        myChart.setOption({
          series: {
            id: 'pie',
            label: {
              formatter: '{b}: {@[' + dimension + ']} ({d}%)'
            },
            encode: {
              value: dimension,
              tooltip: dimension
            }
          }
        });
      }
    });
    // KintoneRestAPIClient で該当アプリの全レコードのデータを取得
    const kintoneRecord = new KintoneRestAPIClient();
    const rcOption = {
      app: app_id, // ご利用のアプリの id に変更してください
      query: 'order by date asc'
    };
    kintoneRecord.record.getAllRecords(rcOption).then((res) => {
      // グラフに表示するために、取得したデータを整形
      const graphData = {channel1: ['東京'], channel2: ['千葉'], channel3: ['埼玉'], channel4: ['神奈川'], channel5: ['栃木']};
      const dateArray = ['チャネル'];
      for (const record of res) {
        const dateKey = record.date.value;
        graphData.channel1.push(record.channel1.value);
        graphData.channel2.push(record.channel2.value);
        graphData.channel3.push(record.channel3.value);
        graphData.channel4.push(record.channel4.value);
        graphData.channel5.push(record.channel5.value);
        dateArray.push(dateKey);
      }
      // グラフの詳細を指定し、取得した売上数値をグラフの dataset に指定
      const option = {
        legend: setting.legend,
        title: {
          text: '地域別売上統計',
          left: 'center'
        },
        tooltip: {
          trigger: 'axis',
          showContent: false
        },
        dataset: {
          source: [
            dateArray,
            graphData.channel1,
            graphData.channel2,
            graphData.channel3,
            graphData.channel4,
            graphData.channel5
          ]
        },
        xAxis: {type: 'category'},
        yAxis: {gridIndex: 0, name: '売上(万円)'},
        grid: setting.grid,
        series: [
          {type: 'line', smooth: true, seriesLayoutBy: 'row'},
          {type: 'line', smooth: true, seriesLayoutBy: 'row'},
          {type: 'line', smooth: true, seriesLayoutBy: 'row'},
          {type: 'line', smooth: true, seriesLayoutBy: 'row'},
          {type: 'line', smooth: true, seriesLayoutBy: 'row'},
          {
            type: 'pie',
            id: 'pie',
            radius: setting.pie.radius,
            center: setting.pie.center,
            // 円グラフの初期値として2022-01-07の数値を指定
            label: {
              formatter: '{b}: {@2022-01-07} ({d}%)'
            },
            encode: {
              itemName: 'チャネル',
              value: '2022-01-07',
              tooltip: '2022-01-07'
            }
          }
        ]
      };
      myChart.setOption(option);
    }).catch((err) => {
      // 該当アプリのレコード取得が失敗したときに、エラーを表示
      document.getElementById('graph').innerText = 'データの取得に失敗しました。';
    });
  };
  // モバイル/PCの場合の、ポータルの上側の空白部分の要素を取得し、その要素にグラフを表示する処理
  const init = (setting) => {
    if (document.getElementById('graph') !== null) {
      return;
    }
    const graph = document.createElement('div');
    graph.id = 'graph';
    graph.style = setting.style;
    if (setting.type === 'mobile') {
      kintone.mobile.portal.getContentSpaceElement().appendChild(graph);
    } else {
      kintone.portal.getContentSpaceElement().appendChild(graph);
    }
  };
})();

ECharts の基本的な使い方

この部分で、ECharts のグラフの基本的な使い方を解説します。

今回のサンプルコードで利用した各グラフの使い方については、以下の ECharts 公式サンプルコードを確認してください。

グラフの初期化

次のサンプルコードのように、ECharts のインスタンスの作成は echarts.init() (External link) で行います。
複数のインスタンスを一度に初期化しないように気を付けてください。

1
const chart = echarts.init(option);

「echarts_app.js」の 65 行目〜 69 行目では、echarts.init() で円グラフのインスタンスを生成しています。

65
66
67
68
69
// スペースフィールドに、円グラフを生成
const report_div = document.createElement('div');
report_div.id = 'graph';
report_div.style = setting.style;
const myChart = echarts.init(report_div);
グラフの描画

次のサンプルコードのように、グラフのレンダリングは setOption() (External link) で行います。
グラフのすべてのパラメーターとデータを setOption() から変更できます。

1
2
3
4
5
6
7
chart.setOption({
  // ...
  series: [{
    name: 'uuu'
    // ...
  }]
});

「echarts_portal.js」の 85 行目〜 132 行目では、setOption() でグラフのパラメーターとデータを指定しています。

 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// グラフの詳細を指定し、取得した売上数値をグラフの dataset に指定
const option = {
  legend: setting.legend,
  title: {
    text: '地域別売上統計',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis',
    showContent: false
  },
  dataset: {
    source: [
      dateArray,
      graphData.channel1,
      graphData.channel2,
      graphData.channel3,
      graphData.channel4,
      graphData.channel5
    ]
  },
  xAxis: {type: 'category'},
  yAxis: {gridIndex: 0, name: '売上(万円)'},
  grid: setting.grid,
  series: [
    {type: 'line', smooth: true, seriesLayoutBy: 'row'},
    {type: 'line', smooth: true, seriesLayoutBy: 'row'},
    {type: 'line', smooth: true, seriesLayoutBy: 'row'},
    {type: 'line', smooth: true, seriesLayoutBy: 'row'},
    {type: 'line', smooth: true, seriesLayoutBy: 'row'},
    {
      type: 'pie',
      id: 'pie',
      radius: setting.pie.radius,
      center: setting.pie.center,
      // 円グラフの初期値として2022-01-07の数値を指定
      label: {
        formatter: '{b}: {@2022-01-07} ({d}%)'
      },
      encode: {
        itemName: 'チャネル',
        value: '2022-01-07',
        tooltip: '2022-01-07'
      }
    }
  ]
};
myChart.setOption(option);

「echarts_portal.js」の 48 行目〜 65 行目では、折れ線グラフの updateAxisPointer イベントで受け取ったパラメーターとデータを setOption() で円グラフに指定しています。

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
myChart.on('updateAxisPointer', (event) => {
  const xAxisInfo = event.axesInfo[0];
  if (xAxisInfo) {
    const dimension = xAxisInfo.value + 1;
    myChart.setOption({
      series: {
        id: 'pie',
        label: {
          formatter: '{b}: {@[' + dimension + ']} ({d}%)'
        },
        encode: {
          value: dimension,
          tooltip: dimension
        }
      }
    });
  }
});
ECharts のイベント

次のサンプルコードのように、ECharts は on() (External link) を使って、イベントハンドラーをバインドできます。
ECharts には 2 種類のイベントがあります。
mouse events (External link) の場合、マウスがチャート内の特定の要素をクリックやマウスオーバーなどをするときにトリガーされます。
dispatchAction (External link) の場合、dispatchAction が呼び出された後にトリガーされます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
chart.setOption({
  // ...
  series: [{
    name: 'uuu'
    // ...
  }]
});
chart.on('mouseover', {seriesName: 'uuu'}, () => {
  // seriesName が 'uuu' の要素がマウスオーバーされたことをトリガーに、処理を実行
});

「echarts_portal.js」の 48 行目〜 65 行目で、折れ線グラフの updateAxisPointer イベントをトリガーに円グラフを生成しています。

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
myChart.on('updateAxisPointer', (event) => {
  const xAxisInfo = event.axesInfo[0];
  if (xAxisInfo) {
    const dimension = xAxisInfo.value + 1;
    myChart.setOption({
      series: {
        id: 'pie',
        label: {
          formatter: '{b}: {@[' + dimension + ']} ({d}%)'
        },
        encode: {
          value: dimension,
          tooltip: dimension
        }
      }
    });
  }
});

動作の確認

サンプルデータの追加

グラフに表示するデータを「売上管理アプリ」に追加してください。
次のサンプルデータを参考にしてください。

ライブラリファイルの準備

今回利用するライブラリは、次の 2 つです。

  • Apache ECharts (External link)
    今回は v5.3.0 を利用します。
    詳細は 公式ドキュメント (External link) を参照ください。
    以下のリンクより「echarts.common.js」ファイルをダウンロードしてください。
    https://github.com/apache/echarts/blob/5.3.0/dist/echarts.common.js (External link)

  • @kintone/rest-api-client (External link)
    今回は v2.0.33 を利用します。
    詳細は 公式ドキュメント を参照ください。
    以下のリンクを利用しますので、控えておいてください。
    https://unpkg.com/@kintone/rest-api-client@2.0.33/umd/KintoneRestAPIClient.min.js

    @kintone/rest-api-client は、 kintone REST API を JavaScript で扱う際に必要な処理をまとめたライブラリです。
    本クライアントで用意されたメソッドを呼び出すだけで kintone REST API を実行できるので、コードの記述量を減らすことができます。
    今回は、レコードを一括処理メソッドを利用しています。
    詳細は kintone JavaScript Client を確認してください。

カスタマイズの適用

さきほど用意したライブラリとサンプルコードを kintone 環境に適用します。

information

サンプルコードを正常に動作させるために、ライブラリファイルを先に読み込む必要があります。
また、サンプルコードはモバイル表示に対応しています。
モバイル画面にグラフを表示させたい場合は、同じサンプルコードを「モバイル用の JavaScript ファイル」にもアップロードしてください。

レコード詳細画面/一覧画面にグラフを表示するカスタマイズの適用

「売上管理アプリ」の設定画面を開き、「設定」タブの「JavaScript / CSS でカスタマイズ」をクリックします。
以下の画像を参考に、今回利用するライブラリのファイルと先ほど作成した JavaScript プログラムを設定します。

設定を保存後、アプリを更新し、レコードの詳細/一覧画面を表示します。
完成イメージのようにグラフが表示されれば成功です。

ポータルにグラフを表示するカスタマイズの適用

kintone のシステム管理画面を開き、「JavaScript / CSS でカスタマイズ」をクリックします。
以下の画像を参考に、今回利用するライブラリのファイルと先ほど作成した JavaScript プログラムを設定します。

information

ポータルのカスタマイズは、kintone のシステム管理から適用する必要があります。
kintone システム管理画面を開く (External link) 方法はリンクを参照してください。

設定を保存後、アプリを更新し、ポータル画面を表示します。
完成イメージのようにグラフが表示されれば成功です。

おわりに

今回の記事は、Apache ECharts を使って kintone のアプリやポータル画面に統計ダッシュボードを表示するカスタマイズを紹介しました。
他にも チャート/ガントチャート について紹介している記事がありますので、興味がある方はぜひ試してみてください。

information

この Tips は、2022 年 3 月版 kintone で動作を確認しています。