JavaScript
GoogleAppsScript
gas
GoogleSpreadSheet
vis.js

GoogleAppsScriptで業務システム作るならvis.jsが良いんじゃないかな?

ガチPGじゃない情シスの悩み

上司から色々なレポートを頼まれてSpreadsheetでなんとかお茶お濁せるうちはまだ良いのよ。
でもGoogleDrive上の複数シートを横断的に集計したり、しかもGoogleCalendarと連携してたり、その上複雑奇怪な表現のレポートを求められても魔改造Spreadsheetでは限界があるわけで、だからといって個別のレポートに対してWebサービス作ってたらキリがないよぅ…ふえぇ…。

というそんなアナタ!
GoogleAppsScriptでちょっとした業務システム作るなら vis.js のTimelineが良いんじゃないかな?

作ったヤツ

キャプチャ.PNG
DEMO: https://script.google.com/macros/s/AKfycbxT6jrO68LS79H1XDhZN3Gx_as5eUSYIc87-Tpi5Odkb7nnvZk/exec

ソースの説明

GAS側のソース

code.gs
/**
* 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側のソース

visjs_timeline.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でチンしてポンという手もあります。

https://qiita.com/howdy39/items/0e799a9bfc1d3bccf6e5

code.gs側の関数を使ってシート情報を得る部分

GAS特有の構文
    // スプレッドシートから、グループとitemの情報を読み出し
    google.script.run.withSuccessHandler(setGroups).getJSONFromSheet('groups');
    google.script.run.withSuccessHandler(setItems).getJSONFromSheet('items');

HTML側のGAS要素はここだけです。
ここの部分を他の一般的なAJAXに置き換えれば、別のプラットホームでも動きます。

ローカライズに関して

vis.jsのTimelineの日本語化に関しては日本語情報が無かったので補足しときます。

日付表示の部分だけど、そもそも
weekday.PNG
こんな表示で分かるわけないじゃん!アメリカ語怖い!
このまま社内展開なんかしたら「日本語で日付表示して!」と突っ込まれることは250%明らか!
日本人なら 2018年5月1日(火)とキチっと表示させるべきである!

ってなわけで細かくはvis.jsのローカライズを読んでもらうとして、要は moment.js の moment-with-locales.min.js を読み込むと多国語対応できるらしいので、vis.jsが読み込まれる前に読み込み。
あとは

locale設定
            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は適度にユルい設計でライセンスも優しいため、自分にはこのくらいの規模感がシックリ来ました。

今回はかなりシンプルな作例を記事にしましたが、カスタマイズに関してまだ書き残した部分も多いので、気が向いたらもう少し書きます。