Google Apps ScriptとVis.jsでタイムラインを作る【GAS】
前回、Google Apps ScriptとTimelineのライブラリを使って、スプレッドシート連動のタイムラインを描くことが出来ました。しかし、前回のライブラリは弱点があり、日付のパーツを上部に固定化したり、またタイムラインのメイン部分だけを縦スクロールさせるといった事ができない為、データ量が多くなると色々と弊害もありました。
その後継ライブラリであるVis.jsではこの辺りが解消されてると聞いたので、今回はVis.jsでタイムラインを描いてみたいと思います。
目次
今回利用するスプレッドシート等
今回のライブラリは前回のものとは異なり、Visualization APIを利用していませんので、あえて追加する必要はありません。今回のライブラリを使って解決したい前回の課題は
- タイムライン部分の縦スクロールを実現
- 同時に日付部分を上部に固定化
- 左側の項目部分をグループに割り当てたIDでソート
になります。
Vis.jsはタイムラインを描く機能を持っていますがそれだけでなくネットワーク図や通常の2Dグラフを描いたり、3Dグラフも描ける総合グラフライブラリになっていますので、他の用途でも利用が可能です。
図:タイムライン表示してみた
データ構造
今回はグループでまとめる機能も利用しています。そのため前回のタイムラインよりもデータ構造が複雑になっています。よって実際に運用する場合にはタスクの作成部分に於いて、入力を簡単にするUIを別途用意したほうが良いのではないかと思います。
グループシート
グループシートはVis.jsの左サイドの項目リストで利用するグループを規定するものになります。
- ID列でソートが掛かるように設計しています。
- 大分類が項目名になるのですがこれは必ず1つ中分類の無いレコードで用意する必要があります。
- 中分類に値が入ってる場合、大分類にぶら下がるグループとしてサブグループの項目名になります。
- 同じ大分類が複数ある場合はネストしてくれます。無い場合にはネストの無いグループ項目になります。
タスクシート
今回のアプリの一番中心になるタスク一覧。1つずつセットしていきます。
- グループIDがグループシートのどこの項目にぶら下がるのかを指定します。
- ネストグループのあるもののみ、大分類のみのレコードを用意し、その大分類にぶら下がる子グループの最小開始日と最大終了日を入れておくと良いです。
- 開始日と終了日のレンジでタイムラインのバーが表示されるようになっています
- カラーで色を選択することでタイムラインのカラーを指定できます。
図:グループIDとタスク名のセット方法がポイント
設定シート
設定シートはタスクシートでの担当者割当で使ってるだけのセレクトオプションのリストになります。今回はこの値は特に何も利用していないので空っぽでも何ら問題ありません。
classシート
classシートはタスクシートのカラーで利用してるセレクトオプションのリストになります。それぞれの色の名前がHTML側のCSSで設定してるものと対応しており、タイムラインのバーの色に反映されます。
ソースコード
GAS側
//スプレッドシートのデータを取得して返す function requery(){ //シートIDを取得する let Prop = PropertiesService.getScriptProperties(); let ssid = Prop.getProperty("sheetid"); let ssman = SpreadsheetApp.openById(ssid); let grpSheet = ssman.getSheetByName("グループ"); let itemSheet = ssman.getSheetByName("タスク"); //シートデータを取得する let ss = grpSheet.getRange("A2:C").getValues(); let ss2 = itemSheet.getRange("A2:G").getValues(); //グループを作成する let groups = makeGroup(ss); //タスクを作成する let tasks = makeItems(ss2); //データを返す return JSON.stringify([groups,tasks]); } //グループを作成する function makeGroup(array){ //グループの元になるユニークなタスク名を抽出する let unique = []; for(let i = 0;i<array.length;i++){ //レコードを一個取り出す let rec = array[i]; //大分類だけを取り出す let taskname = rec[1]; //uniqueに追加 unique.push(taskname) } //unique配列の中から重複を取り除く。 const uniqueArray = Array.from(new Set(unique)); //グループ返却用配列を用意する let arrGroup = []; let nested = []; let single = []; //ユニーク配列を元にarrayの中身からネストした項目を取り出す for(let j = 0;j<uniqueArray.length;j++){ //チェック変数を0にする let check = 0 //タスクグループを一個取り出す let taskman = uniqueArray[j]; //一次配列を用意 let temparr = {}; let nestarr = []; let singleman = ""; //ネストチェック for(let k = 0;k<array.length;k++){ //レコードを一個取り出す let rec = array[k]; //taskmanと一致する項目があるかどうか? if(taskman == rec[1]){ //チェックが0ならば、普通に追加する if(check == 0){ temparr = { id: rec[0], content: rec[1], } singleman = rec[1] }else{ //2つ目以降がみつかったのでネストとして追加 nestarr.push(rec[0]) //singleにも追加していく single.push( { id: rec[0], content: rec[2], } ) } //チェック変数を一個加える check = check + 1; } } //nestarrが空かどうか? if(nestarr.length == 0){ //nestが無いので項目をsinglemanに書き換える temparr.content = singleman; //singleにtemparrを追加する single.push(temparr); }else{ //temparrにnestedGroupsを追加する temparr.nestedGroups = nestarr; //nestedに追加する nested.push(temparr) } } //配列を結合 let retarray = nested.concat(single); //グループデータを返す return retarray; } //アイテムを作成する function makeItems(array){ //返却用配列 let itemarr = [] //アイテムを構築する for(let i = 0;i<array.length;i++){ //レコードを一個取り出す let rec = array[i]; //一次配列を構築 let temparr = { id: rec[0], group: rec[1], content: rec[2], start: rec[4], end: rec[5], type: "range", className: rec[6] } //配列に追加 itemarr.push(temparr); } //データを返す return itemarr; }
- メインの関数はrequery関数です。シートから取得したデータよりグループとアイテムを作成する関数を呼び出します。
- makeGroup関数はグループシートよりVis.jsで利用するグループデータを生成します。親グループにぶら下がる子グループがある場合にだけnestedGroupsを追加し、対象の子グループのIDを配列で追加します。
- makeItems関数はタスクシートのデータを単純にVis.jsで使うアイテムデータを生成します。classNameがタスクの色指定です。
- 実際に利用する場合はスプシのメニューよりタイムライン=>セットアップをクリックしてスプシのIDをスクリプトプロパティに格納する必要があります(初回クリックで認証時は格納されないので注意。もう一度実行しよう)
HTML側
<head> <!-- jQueryとTimeLine関係 --> <script src="//code.jquery.com/jquery-1.10.2.js"></script> <!-- Vis.js関係 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment-with-locales.min.js"></script> <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"/> <style type="text/css"> body, html { font-family: arial, sans-serif; font-size: 11pt; } #visualization { box-sizing: border-box; width: 100%; height: 300px; } .red{ background: #ffb3b3; box-shadow: 3px 3px 3px #bfbfbf; } .green{ background: #c4ffcb; box-shadow: 3px 3px 3px #bfbfbf; } .blue{ background: #b0ddff; box-shadow: 3px 3px 3px #bfbfbf; } .yellow{ background: #fbffc4; box-shadow: 3px 3px 3px #bfbfbf; } .purple{ background: #f0c7ff; box-shadow: 3px 3px 3px #bfbfbf; } .gray{ background: #ebffe0; box-shadow: 3px 3px 3px #bfbfbf; } .orange{ background: #f2d3b8; box-shadow: 3px 3px 3px #bfbfbf; } </style> <script type="text/javascript"> var timeline; var items; var groups; var elem; //リサイズ時にウィンドウにフィットさせる window.onresize = function(){ setGridHeight(); } //gridのサイズを自動でウィンドウにフィットする $(document).ready(function () { setGridHeight(); }); //座席表の高さを自動補正する関数 function setGridHeight() { //ウィンドウの高さを算出(90%で設定) let height = $(window).height() - 15; //オプション反映 try{ timeline.setOptions({ height: height, }); }catch(e){ } } //vis.jsのオプション var options = { groupOrder: "content", orientation:"top", height:"100px", minHeight: "200px", horizontalScroll:true, verticalScroll:true, //日本語化する locale: 'ja', stack: true, stackSubgroups: true, //日本語化用フォーマット 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: '' } }, //グループのIDで昇順ソートする groupOrder: function (a, b) { return a.id - b.id; }, }; //初期化実行 initialize(); //スプシ一覧を取得する function initialize() { google.script.run.withSuccessHandler(onSuccess2).requery(); } //シートデータを取得する function onSuccess2(data){ //データを取得する let json = JSON.parse(data); //現在時刻 var now = moment().minutes(0).seconds(0).milliseconds(0); //グループを用意する groups = new vis.DataSet(json[0]); //タスクを用意する items = new vis.DataSet(json[1]); //タイムラインを反映する elem = document.getElementById("mytimeline"); timeline = new vis.Timeline(elem, items, groups, options); //データの反映 timeline.setGroups(groups); timeline.setItems(items); //リサイズする setGridHeight(); } </script> </head> <body style="font-family: Arial;border: 0 none;"> <div id="mytimeline"></div> </body>
- cssにて各カラーのClassに対する色設定を追加しています。
- ウィンドウリサイズ時に実行するsetGridHeight関数では、生成したTimelineに対してsetOptionで正しい高さを再指定します。
- Vis.jsはオリジナルはLocaleに日本語が無いため、「moment-with-locales.min.js」を読み込ませています。
- optionsのverticalScrollをtrueにすることで縦スクロール対応になります。
- optionsのorientationをtopにすることで日付エリアが上に固定化されます。
- optionsのformatでLocaleの日本の場合の置き換える単語を列挙しています。
- optionsのgroupOrderにてオリジナルにはないグループのIDで項目をソートしています。
- データをGAS側から取得し、groupとItemのそれぞれに値をはめ込んだら、最後にsetGroupsやsetItemsにて反映します。
サンプル表示
- 左サイドの項目エリアはグループシートのIDを基準にソートが掛かっています
- また項目エリアでスクロールをすると縦にスクロール出来るため、サイズ固定の場合に下の方が表示できないといった事がありません。
- 上部に日付エリアを固定化
- 日本語化してあるので、使う上で英語UIではないため馴染みやすい
- 各タスクの色は指定したclassの名前で色指定が反映されている
- ウィンドウをリサイズしても描画エリアもきちんとリサイズするように調整出来ています。
関連リンク
- Network with 100% height set does not render correctly #1832
- Workaround for vertical scrolling in Vis.js
- VisJS timeline verticalScroll and horizontalScroll = "true" don't work together in global options
- Can't change the div-height vis timeline
- Network - can't set height to a percentage of display #2978
- How to automatically resize height of visjs Network component when window resized?
- GoogleAppsScriptで業務システム作るならvis.jsが良いんじゃないかな?
- VisJS Timeline: sorting items in timeline
- Vis.js focus on the Current Time (Red Line)