LoginSignup
5
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-14

この記事は ニフティグループ Advent Calendar 2021 15日目の記事です。

概要

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

目指すゴール

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

このデータを見やすく表示することを今回のゴールとします。
image.png

image.png

データ全体
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)

結果

ここまでのプログラムを実行すると、下のような結果が得られます。
これでどのデータがどこに依存しているかがひと目で分かるようになりました!!
image.png

ここまでのプログラム全体
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)
        )
    }
}

まとめ

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

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3