SpreadSheetで階層構造グラフをパースして可視化

この記事は [ニフティグループ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nifty) 15日目の記事です。

# 概要
システムからcsvで出力した階層構造を持つデータを手元で可視化したい！はよくある話だと思います。
今回はspread sheet上のデータで階層構造をパースして、見やすく表示することにします。

# 目指すゴール
以下のようなデータをテスト用として用意します。
想定としてはシステムから出力した値をスプレッドシート上に取り込んだ形です。
生データのままではどれがどのデータに依存しているのかがよくわかりません。

このデータを見やすく表示することを今回のゴールとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/ed33be7c-4ec0-964a-4bee-2a66b55361cb.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/b070f6a7-df06-4e75-9d9b-d8fbef1a0224.png)

<details><summary>データ全体</summary><div>

```
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
```

</div></details>

# 実装
今回はGASを使ってデータのパース、可視化をしていきます。
GASの使い方は[【初心者向け】GASでニュース記事をSlackに通知するアプリを作ってみた～前編～](https://qiita.com/sakauchi_riki/items/3194e6ae2fdb7916784a) をご参照ください。

まずはスプレッドシートから値を読み込みます。
あとで扱いやすくするためにデータはオブジェクト型へ変換しておきます。

```js
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])
  }})
```

上のデータを出力するとこんな感じです。

```js
[{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 でツリー階層をデータで持たせる](https://qiita.com/es-row/items/4d654d4272564f8c5528)

```js
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)
        )
    }
}
```

次はすべてのデータを先程定義した木に登録していきます。

```js
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)
})
```

あとは深さ優先で値を出力していきます。

```js
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)
```

# 結果
ここまでのプログラムを実行すると、下のような結果が得られます。
これでどのデータがどこに依存しているかがひと目で分かるようになりました！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/098142f7-62f5-ba97-7fa3-7236b0b4367e.png)


<details><summary>ここまでのプログラム全体</summary><div>

```js:parse.js
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)
        )
    }
}
```
</div></details>

# まとめ
GASでスプレッドシートの内容をパースして、階層構造として出力しました。
図と比べると若干見づらいですが、階層構造としてはかなりわかりやすい形にできたので満足です。
今回は目で見るために全データをスプレッドシート上に出力しましたが、子孫探索を書いて特定のノードの下だけを全部出力するプログラムを書いても後で使えそうです。
最近 python ばっかり書いて js を書く機会が全然なかったので思い出すいい機会になりました。
GASがつかえるとスプレッドシートのデータをごにょごにょするのが、関数よりも簡単なので是非とも試してみてください。