ガチPGじゃない情シスの悩み
上司から色々なレポートを頼まれてSpreadsheetでなんとかお茶お濁せるうちはまだ良いのよ。
でもGoogleDrive上の複数シートを横断的に集計したり、しかもGoogleCalendarと連携してたり、その上複雑奇怪な表現のレポートを求められても魔改造Spreadsheetでは限界があるわけで、だからといって個別のレポートに対してWebサービス作ってたらキリがないよぅ…ふえぇ…。
というそんなアナタ!
GoogleAppsScriptでちょっとした業務システム作るなら vis.js のTimelineが良いんじゃないかな?
作ったヤツ
DEMO: https://script.google.com/macros/s/AKfycbxT6jrO68LS79H1XDhZN3Gx_as5eUSYIc87-Tpi5Odkb7nnvZk/exec
ソースの説明
GAS側のソース
/**
* WebAPIとして呼ばれる場合のエントリポイント
*/
function doGet() {
var template = HtmlService.createTemplateFromFile('visjs_timeline.html');
return template.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
/**
*
* @return {Array<string>} data jsonデータを返す
*/
function getJSONFromSheet(sheet) {
console.time('getJSONFromSheet("' + sheet + '")');
var result = convSheet('13BOOY-ygYM8oPTdX4RiEtcapZ5ufJMIxV-q5AYrDinw', sheet);
console.timeEnd('getJSONFromSheet("' + sheet + '")');
return JSON.stringify(result);
}
/**
* sheetidとシート名からをデータ読み取り、オブジェクトデータとして返す
*/
function convSheet(spreadsheetId, sheetName) {
var ss = SpreadsheetApp.openById(spreadsheetId);
var sheet = ss.getSheetByName(sheetName);
return getSheetDataJSON(sheet);
}
/**
* 該当のsheetを読み取り、オブジェクトデータとして返す
* データの1行目の各列の文字列を要素名とする事を期待しているため、空文・重複は禁止
* シート全体を読み込んで処理するので、余ったカラムやセルは念のため詰めておいてください
* セル内にコンマ( , )が入っていた場合、split(',')してarrayとして返します
*/
function getSheetDataJSON(sheet) {
var values = sheet.getDataRange().getValues();
var columns = values[0].length;
var rows = values.length;
var jsonArray = [];
for (var i = 1; i < rows; i++) { // 先頭行はIndexなので2行目から
var json = {};
for (var j = 0; j < columns; j++) {
var str = new String(values[i][j]);
if (str.length == 0) continue;
if (str.indexOf(',') == -1) {
json[values[0][j]] = str;
} else {
json[values[0][j]] = str.split(",");
}
}
jsonArray.push(json);
}
return jsonArray;
}
GASのコードの主な役割は、vis.js用のグループ設定や、アイテムの配列をシートから読み込んでJSONとして返すだけです。
(面倒なのでシートID直打ちしてますが、実際使う際はこのIDは自分のSpreadsheetでやってくださいおながいします)
例外とかも綺麗サッパリやってないので、ガチ勢の方はよしなにお願いします。
あ、シートも共有しときますんで、設定内容はそちらでご確認ください。
https://docs.google.com/spreadsheets/d/13BOOY-ygYM8oPTdX4RiEtcapZ5ufJMIxV-q5AYrDinw/edit?usp=sharing
シートの1行目のヘッダ文字列は、そのままvis.jsが必要とする項目名を入力しています。
データの詳細はvis.jsのDataFormatの部分を見てもらうとして、動作するために最小限必要な情報は
{
id: String or Number
content: String
}
{
content: String
start: Date or number
string or Moment
}
くらいのもので、残りの設定項目はオプションです。
今回はスケジュールとかでよく使いそうな項目をそれっぽく設定してます。
HTML側のソース
<!DOCTYPE html>
<html>
<head>
<title>GASでVis.jsのTimelineを使う</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment-with-locales.min.js"></script>
<!-- 必ずvis.jsを読み込む前に moment-with-locales.js を読み込むこと -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css"
/>
</head>
<body onresize="/*timeline.checkResize();*/">
<div id="mytimeline"></div>
<script>
// specify options
var options = {
stack: true,
stackSubgroups: true,
start: new Date('2018/4/1'),
zoomMax: 24 * 60 * 60 * 1000 * 30 * 7,
zoomMin: 24 * 60 * 60 * 1000 * 30,
verticalScroll: true,
// デフォルトではグループにソート機能は無いので
// 独自プロパティを用意してソート用のキーにする。
groupOrder: function (a, b) {
return a._order - b._order;
},
locale: 'ja', // vis.jsのデフォルトにはjaは無いがmoment-with-locales.jsを読むと使える
format: {
minorLabels: {
millisecond: 'SSS',
second: 's',
minute: 'HH:mm',
hour: 'HH:mm',
weekday: 'M/D(dd)',
day: 'M/D(dd)',
week: 'w',
month: 'MMM',
year: 'YYYY'
},
majorLabels: {
millisecond: 'HH:mm:ss',
second: 'M/D HH:mm',
minute: 'M/D(dd)',
hour: 'M/D(dd)',
weekday: 'YYYY年',
day: 'YYYY年M月',
week: 'YYYY年M月',
month: 'YYYY年',
year: ''
}
}
};
// とりあえず中身無しでTimelineを作成
var container = document.getElementById('mytimeline');
var timeline = new vis.Timeline(container, null, options);
// スプレッドシートから、グループとitemの情報を読み出し
google.script.run.withSuccessHandler(setGroups).getJSONFromSheet('groups');
google.script.run.withSuccessHandler(setItems).getJSONFromSheet('items');
function setGroups(SSValues) {
var g_groups = JSON.parse(SSValues);
var groups = new vis.DataSet(g_groups);
timeline.setGroups(groups);
}
function setItems(SSValues) {
var g_items = JSON.parse(SSValues)
var items = new vis.DataSet(g_items);
timeline.setItems(items);
}
</script>
</body>
</html>
ここも例外やエラー処理をすっ飛ばしているのでよしなに。
以下細な補足事項などを。
GAS使ってサービス公開する時の注意
多分こんな記事タイトルに惹かれて読んでるGAS勢の方々知ってることとは思いますが念のため補足します。
外部サイトのJavascriptライブラリやCSS、WebFontなんかをGASで使う場合、XDR(Cross-Domain Requests)に対処できていないサイトからは読み込めません。
マイナーなライブラリなんかは作者さんのWebsiteとかでホストしてたりする事もあるけど、公開サイトがhttps化されてなかったりするとエラーになります。
まぁ最近はcloudflareやgoogleのCDNに有名どこのリソースはホストされていると思うので、そっちを使うのがベター。
(もしこの記事の公開以後にvis.jsやmoment.jsのCDN読めなくなってたら、適当にソースを変更してください)
外部読み込みが使えない場合、GAS上にソースを丸ごと上げて読み込ませる事もできるので、最悪それでもOK。
現行環境ならclaspが使えるようになったので、Webpackでチンしてポンという手もあります。
code.gs側の関数を使ってシート情報を得る部分
// スプレッドシートから、グループとitemの情報を読み出し
google.script.run.withSuccessHandler(setGroups).getJSONFromSheet('groups');
google.script.run.withSuccessHandler(setItems).getJSONFromSheet('items');
HTML側のGAS要素はここだけです。
ここの部分を他の一般的なAJAXに置き換えれば、別のプラットホームでも動きます。
ローカライズに関して
vis.jsのTimelineの日本語化に関しては日本語情報が無かったので補足しときます。
日付表示の部分だけど、そもそも
こんな表示で分かるわけないじゃん!アメリカ語怖い!
このまま社内展開なんかしたら「日本語で日付表示して!」と突っ込まれることは250%明らか!
日本人なら 2018年5月1日(火)とキチっと表示させるべきである!
ってなわけで細かくはvis.jsのローカライズを読んでもらうとして、要は moment.js の moment-with-locales.min.js を読み込むと多国語対応できるらしいので、vis.jsが読み込まれる前に読み込み。
あとは
locale: 'ja', // vis.jsのデフォルトにはjaは無いがmoment-with-locales.jsを読むと使える
format: {
minorLabels: {
millisecond: 'SSS',
second: 's',
minute: 'HH:mm',
hour: 'HH:mm',
weekday: 'M/D(dd)',
day: 'M/D(dd)',
week: 'w',
month: 'MMM',
year: 'YYYY'
},
majorLabels: {
millisecond: 'HH:mm:ss',
second: 'M/D HH:mm',
minute: 'M/D(dd)',
hour: 'M/D(dd)',
weekday: 'YYYY年',
day: 'YYYY年M月',
week: 'YYYY年M月',
month: 'YYYY年',
year: ''
}
}
みたいな感じで、お気に召すままのフォーマットを組めば、めでたく「月月火水木金金」の曜日表示が出るようになります。
まとめ
他にもちょっとした注意点もあるけど、かなりシンプルな例にしたのでソースのコメントをご参照ください。
社内ツール作成する際に、横線で管理するタイプのスケジューラやガントチャート的なものが必要になる事が多く、最初はガントチャートのライブラリを漁りまくってたのですが、扱いやすい手ごろな規模のモノが無く途方に暮れていました。
そんな中vis.jsのTimelineのサンプルを見つけた時「これだ!!」……と。
実のところ、以前からvis.jsの存在は認識していたのですが、Web上のvis.js解説はネットワーク図の記事か多く、Timelineの汎用性に気付いていませんでした。
個人的には他のガチ・ガントチャートライブラリは融通が利きにくい印象で、しかも便利そうな機能が有償だったしてぐぬぬってなります。
その点、vis.jsは適度にユルい設計でライセンスも優しいため、自分にはこのくらいの規模感がシックリ来ました。
今回はかなりシンプルな作例を記事にしましたが、カスタマイズに関してまだ書き残した部分も多いので、気が向いたらもう少し書きます。