この記事は ニフティグループ Advent Calendar 2021 15日目の記事です。
概要
システムからcsvで出力した階層構造を持つデータを手元で可視化したい!はよくある話だと思います。
今回はspread sheet上のデータで階層構造をパースして、見やすく表示することにします。
目指すゴール
以下のようなデータをテスト用として用意します。
想定としてはシステムから出力した値をスプレッドシート上に取り込んだ形です。
生データのままではどれがどのデータに依存しているのかがよくわかりません。
このデータを見やすく表示することを今回のゴールとします。
↓
データ全体
LABEL NAME PARENT
ROOT ルート
10 c1 ROOT
20 c2 10
30 c3 20
40 c4 30
50 c5 40
51 c51 50
52 c52 50
53 c53 50
54 c54 50
55 c55 50
56 c56 50
561 c561 56
562 c562 56
563 c563 56
564 c564 56
60 c6 40
601 c601 60
602 c602 60
603 c603 60
604 c604 60
61 c61 40
611 c611 61
612 c612 61
613 c613 61
614 c614 61
615 c615 61
62 c62 40
621 c621 62
622 c622 62
623 c623 62
624 c624 62
625 c625 62
626 c626 62
627 c627 62
628 c628 62
629 c629 62
70 c7 40
701 c701 70
702 c702 70
703 c703 70
704 c704 70
705 c705 70
706 c706 70
実装
今回はGASを使ってデータのパース、可視化をしていきます。
GASの使い方は【初心者向け】GASでニュース記事をSlackに通知するアプリを作ってみた~前編~ をご参照ください。
まずはスプレッドシートから値を読み込みます。
あとで扱いやすくするためにデータはオブジェクト型へ変換しておきます。
const sheet_id = '<シートID>'
const _spreadsheet = SpreadsheetApp.openById(sheet_id);
const input_sheet = _spreadsheet.getSheetByName('シート1');
const v = input_sheet.getRange("A2:AK45").getValues();
const data = v.map(x => {return {
label: String(x[0]),
name: x[1],
parent: String(x[2])
}})
上のデータを出力するとこんな感じです。
[{name=ルート, label=ROOT, parent=}, {parent=ROOT, name=c1, label=10}, {label=20, name=c2, parent=10}, {label=30, parent=20, name=c3}, {name=c4, parent=30, label=40}, {name=c5, label=50, parent=40}, {name=c51, parent=50, label=51}, {parent=50, name=c52, label=52}, {name=c53, label=53, parent=50}, {name=c54, parent=50, label=54}, {label=55, name=c55, parent=50}, {name=c56, parent=50, label=56}, {label=561, name=c561, parent=56}, {name=c562, label=562, parent=56}, {name=c563, label=563, parent=56}, {parent=56, name=c564, label=564}, {name=c6, label=60, parent=40}, {parent=60, name=c601, label=601}, {name=c602, parent=60, label=602}, {label=603, name=c603, parent=60}, {parent=60, label=604, name=c604}, {label=61, name=c61, parent=40}, {parent=61, name=c611, label=611}, {label=612, name=c612, parent=61}, {label=613, name=c613, parent=61}, {name=c614, parent=61, label=614}, {parent=61, name=c615, label=615}, {label=62, name=c62, parent=40}, {label=621, name=c621, parent=62}, {label=622, name=c622, parent=62}, {parent=62, label=623, name=c623}, {label=624, parent=62, name=c624}, {parent=62, label=625, name=c625}, {name=c626, parent=62, label=626}, {name=c627, label=627, parent=62}, {name=c628, label=628, parent=62}, {label=629, name=c629, parent=62}, {label=70, parent=40, name=c7}, {label=701, parent=70, name=c701}, {name=c702, parent=70, label=702}, {label=703, name=c703, parent=70}, {label=704, parent=70, name=c704}, {name=c705, label=705, parent=70}, {name=c706, parent=70, label=706}]
次にデータを処理していくわけですが、データは1つの親と複数の子を持ち、ループ関係がないことから木構造が向いてそうです。
というわけで木構造を定義します。
※参考:Javascript でツリー階層をデータで持たせる
class Node {
constructor(name, code) {
this.name = name;
this.code = code;
this.parent = null;
this.childrenArray = [];
}
addChild(childNode) {
this.childrenArray.push(childNode);
childNode.parent = this;
}
addChildren(childrenArray) {
childrenArray.forEach(x =>
this.addChild(x)
)
}
}
次はすべてのデータを先程定義した木に登録していきます。
function addAllData(tree, data) {
// 追加するデータがないなら終了
if (data.filter(d => d.parent == tree.code).length == 0) return;
// 親と今見ているノードが同じデータをすべて子として登録していく
tree.addChildren(data.filter(d => d.parent == tree.code).map(x => new Node(x.name, x.label)))
// 子を見ていく
tree.childrenArray.forEach(child => {
addAllData(child, data)
})
}
// 親要素がない=root要素を登録
trees = data.filter(x => !x.parent).map(x => new Node(x.name, x.label))
// すべてのデータをroot配下に再帰的に登録していく
trees.forEach(tree => {
addAllData(tree, data)
})
あとは深さ優先で値を出力していきます。
const output_sheet = _spreadsheet.getSheetByName('シート2');
function writeToSpreadsheetWithDfs(tree, depth=0) {
// シート上で階層構造を表現するために、子の深さに応じて空白を埋めた配列を用意する
let pad_array = new Array(depth+1).fill('', 0, depth)
// 配列の最後尾には出力する対象をいれておく
pad_array[pad_array.length -1] = tree.name
// シートの上からデータで埋めていく
output_sheet.appendRow(pad_array)
// 子がいなくなるまで再帰的にデータを出力していく
if (tree.childrenArray.length == 0) return;
tree.childrenArray.forEach(x => writeToSpreadsheetWithDfs(x, depth+1))
}
// root要素から深さ優先で値を出力していく
writeToSpreadsheetWithDfs(trees[0], 0)
結果
ここまでのプログラムを実行すると、下のような結果が得られます。
これでどのデータがどこに依存しているかがひと目で分かるようになりました!!
ここまでのプログラム全体
const sheet_id = '<シートID>'
const _spreadsheet = SpreadsheetApp.openById(sheet_id);
const input_sheet = _spreadsheet.getSheetByName('シート1');
const output_sheet = _spreadsheet.getSheetByName('シート2');
function addAllData(tree, data) {
// 追加するデータがないなら終了
if (data.filter(d => d.parent == tree.code).length == 0) return;
// 親と今見ているノードが同じデータをすべて子として登録していく
tree.addChildren(data.filter(d => d.parent == tree.code).map(x => new Node(x.name, x.label)))
// 子を見ていく
tree.childrenArray.forEach(child => {
addAllData(child, data)
})
}
function writeToSpreadsheetWithDfs(tree, depth=0) {
// シート上で階層構造を表現するために、子の深さに応じて空白を埋めた配列を用意する
let pad_array = new Array(depth+1).fill('', 0, depth)
// 配列の最後尾には出力する対象をいれておく
pad_array[pad_array.length -1] = tree.name
// シートの上からデータで埋めていく
output_sheet.appendRow(pad_array)
// 子がいなくなるまで再帰的にデータを出力していく
if (tree.childrenArray.length == 0) return;
tree.childrenArray.forEach(x => writeToSpreadsheetWithDfs(x, depth+1))
}
function myFunction() {
const v = input_sheet.getRange("A2:AK45").getValues();
const data = v.map(x => {return {
label: String(x[0]),
name: x[1],
parent: String(x[2])
}})
// 親要素がない=root要素を登録
trees = data.filter(x => !x.parent).map(x => new Node(x.name, x.label))
// すべてのデータをroot配下に再帰的に登録していく
trees.forEach(tree => {
addAllData(tree, data)
})
// root要素から深さ優先で値を出力していく
writeToSpreadsheetWithDfs(trees[0], 0)
}
class Node {
constructor(name, code) {
this.name = name;
this.code = code;
this.parent = null;
this.childrenArray = [];
}
addChild(childNode) {
this.childrenArray.push(childNode);
childNode.parent = this;
}
addChildren(childrenArray) {
childrenArray.forEach(x =>
this.addChild(x)
)
}
}
まとめ
GASでスプレッドシートの内容をパースして、階層構造として出力しました。
図と比べると若干見づらいですが、階層構造としてはかなりわかりやすい形にできたので満足です。
今回は目で見るために全データをスプレッドシート上に出力しましたが、子孫探索を書いて特定のノードの下だけを全部出力するプログラムを書いても後で使えそうです。
最近 python ばっかり書いて js を書く機会が全然なかったので思い出すいい機会になりました。
GASがつかえるとスプレッドシートのデータをごにょごにょするのが、関数よりも簡単なので是非とも試してみてください。