Amazonで商品を見るセール会場へ

Googleスプレッドシート│組織図やUI構造の情報をビジュアル化!階層表示する方法

この記事では、Googleスプレッドシートで管理している階層データを、D3.jsを使ってブラウザ上にツリー構造で可視化する方法を解説します。

表示イメージ

スプレッドシート

元データの例です。

階層表示を行う元データとなるGoogleスプレッドシートの例。

スプレッドシートの情報を元に階層表示

スプレッドシートの情報を元に階層表示されたビジュアルイメージ。

多階層のデータ構造にも対応しています。

また、ノードをホバーした際に補足情報をツールチップで表示する仕様にしています。

目次

スプレッドシートの作成

まず、階層構造を定義するスプレッドシートを用意します。

構成例は次のとおりです。

グループ名nodeIdparentIdノード名補足情報
猫カフェにゃんたsample店舗本部住所、責任者、営業時間
猫カフェにゃんたsample/猫管理sample猫管理エリア名前、年齢、性格
猫カフェにゃんたsample/猫管理/ミケsample/猫管理ミケ3歳、甘えん坊、♀
猫カフェにゃんたsample/猫管理/タマsample/猫管理タマ2歳、活発、♂
猫カフェにゃんたsample/設備管理sample設備管理空調、清掃、備品
猫カフェにゃんたsample/予約管理sample 1 Variant予約・顧客管理予約一覧、会員カード

シートを作成したら、メニューの「拡張機能」から「Apps Script」を選択してエディタを開きます。

メニューの拡張機能からApps Scriptを選択してエディタを開く手順の画面。

手順は次のとおりです。

GAS│コードの構成

  • index.htmlで<svg>を配置
  • js.htmlにスタイル設定を記述
  • js.htmlに D3.js による描画ロジックを記述
  • main.gsでスプレッドシートのデータを取得
HTMLやJavaScript、GASファイルで構成されたコードの構成イメージ。

コード全文

各ファイルに以下のコードを貼り付けて保存します。

Googleスプレッドシートの関連操作については、まとめのリンクから確認できます。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>階層ビュー</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
  <?!= include("css"); ?>

</head>
  <body>
    <div id="container">
      <svg></svg>
      <div id="tooltip"></div>      
    </div>
    <?!= include("js"); ?>
  </body>
</html>

css.html

<style>
html,
body {
  height: 100%;
  margin: 0;
}

#container {
  width: 100%;
  height: 100%;
  overflow: auto;
}

svg {
  display: block;
  width: 100%;
}

#tooltip {
  position: absolute;
  display: none;
  pointer-events: none;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  font-size: 12px;
  padding: 4px 8px;
  border-radius: 4px;
  z-index: 1000;
  white-space: pre-wrap;
  max-width: 300px;
}
</style>

js.html

<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

/* ===== 設定値 ===== */
const sheetUrl =
  "https://docs.google.com/spreadsheets/d/★ここにスプレッドシートのID/gviz/tq?tqx=out:json";
const rectSize = { width: 100, height: 30 };
const space    = { width: 140, height: 60, padding: 30 };
const gap      = { x: 40, y: 60 };

/* ===== スプレッドシート → 階層データ ===== */
const buildTree = (rows) => {
  const map = new Map(), childrenMap = new Map();

  rows.forEach((r) => {
    const nodeId = r.c[1]?.v;
    const parentId = r.c[2]?.v || null;
    const name = r.c[3]?.v;
    const comps = r.c[4]?.v || "";

    if (!nodeId || !name) return;

    map.set(nodeId, {
      name,
      comps,
      children: []
    });

    if (parentId) {
      if (!childrenMap.has(parentId)) childrenMap.set(parentId, []);
      childrenMap.get(parentId).push(nodeId);
    }
  });

  childrenMap.forEach((childIds, pid) => {
    if (!map.has(pid)) return;
    childIds.forEach((cid) => map.get(pid).children.push(map.get(cid)));
  });

  return [...map.keys()].filter(
    id => !rows.some(r => r.c[1]?.v === id && r.c[2]?.v)
  ).map(id => map.get(id));
};

/* ===== 座標計算(Y位置) ===== */
const seekParent = (cur, name) => {
  const hrcy = cur.parent.children;
  const tgt = hrcy.find((c) => c.data.name === name);
  return tgt ? { name, hierarchy: hrcy } : seekParent(cur.parent, name);
};
const calcLeaves = (names, cur) => {
  const hs = names.map((n) => seekParent(cur, n));
  const idxs = hs.map((h) => h.hierarchy.findIndex((d) => d.data.name === h.name));
  const sliced = hs.map((h, i) => h.hierarchy.slice(0, idxs[i]));
  return sliced.flat().map((d) => d.value);
};
const defineY = (d, sp) => {
  const leaves = calcLeaves(d.ancestors().map((a) => a.data.name).slice(0, -1), d);
  return leaves.reduce((s, v) => s + v, 0) * sp.height + sp.padding;
};
const definePos = (root, sp) => {
  root.each((d) => {
    d.x = d.depth * sp.width + sp.padding;
    d.y = defineY(d, sp);
  });
};

/* ===== テキスト自動縮小 ===== */
function fitText(textSel, maxWidth, minFont = 6){
  textSel.each(function(){
    const text = d3.select(this);
    let fontSize = parseInt(text.style("font-size")) || 12;
    while (this.getComputedTextLength() > maxWidth && fontSize > minFont){
      fontSize -= 1;
      text.style("font-size", `${fontSize}px`);
    }
  });
}

/* ===== メイン処理 ===== */
fetch(sheetUrl)
  .then((res) => res.text())
  .then((txt) => JSON.parse(txt.match(/google\.visualization\.Query\.setResponse\((.*)\)/s)[1]))
  .then((json) => {
    const rows = json.table.rows.slice(1);
    const trees = buildTree(rows);

    const svg = d3.select("svg");
    const svgWidth = +svg.attr("width") || window.innerWidth;

    let xOffset = 0, yOffset = 0, rowMaxHeight = 0;

    trees.forEach((data) => {
      const root = d3.hierarchy(data);
      d3.tree()(root);
      root.count();
      definePos(root, space);

      const treeWidth =
        (root.height + 1) * space.width + space.padding * 2;
      const treeHeight =
        root.value * rectSize.height +
        (root.value - 1) * (space.height - rectSize.height) +
        space.padding * 2;

      // 折り返し処理
      if (xOffset + treeWidth > svgWidth) {
        xOffset = 0;
        yOffset += rowMaxHeight + gap.y;
        rowMaxHeight = 0;
      }

      const g = svg.append("g")
        .attr("transform", `translate(${xOffset},${yOffset})`);
      xOffset += treeWidth + gap.x;
      rowMaxHeight = Math.max(rowMaxHeight, treeHeight);

      // エッジ描画
      g.selectAll(".link")
        .data(root.descendants().slice(1))
        .enter()
        .append("path")
        .attr("fill", "none")
        .attr("stroke", "#000")
        .attr("d", (d) => {
          const yC = d.y + rectSize.height / 2;
          const yP = d.parent.y + rectSize.height / 2;
          const xM = d.parent.x + rectSize.width + (space.width - rectSize.width) / 2;
          return `M${d.x},${yC} L${xM},${yC} L${xM},${yP} L${d.parent.x + rectSize.width},${yP}`;
        });

      // ノード描画
      const node = g.selectAll(".node")
        .data(root.descendants())
        .enter()
        .append("g")
        .attr("class", "node")
        .attr("transform", (d) => `translate(${d.x},${d.y})`);

      const tooltip = d3.select("#tooltip");

      node
        .on("mouseenter", (event, d) => {
          tooltip
            .style("display", "block")
            .text(d.data.comps || "(コンポーネントなし)");
        })
        .on("mousemove", (event) => {
          tooltip
            .style("left", `${event.pageX + 10}px`)
            .style("top", `${event.pageY + 10}px`);
        })
        .on("mouseleave", () => {
          tooltip.style("display", "none");
        });

      node.append("rect")
        .attr("width", rectSize.width)
        .attr("height", rectSize.height)
        .attr("fill", "#fff")
        .attr("stroke", "#333");

      const txt = node.append("text")
        .text(d => d.data.name)
        .attr("x", 6)
        .attr("y", 20);

      // ツールチップ追加(SVG標準)
      txt.append("title").text(d => d.data.comps || "(コンポーネントなし)");

      fitText(txt, rectSize.width - 8);
    });

    svg.attr("height", yOffset + rowMaxHeight + space.padding);
  });
</script>

main.gs

function doGet () {
  return HtmlService.createTemplateFromFile('index').evaluate();
}

function getPrefabTreeData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");//★ここにシート名
  const data = sheet.getDataRange().getValues();
  const headers = data[0];
  const rows = data.slice(1);

  const nodes = [];
  const edges = [];

  const map = {};

  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];
    const nodeId = row[1]?.toString().trim();
    const parentId = row[2]?.toString().trim();
    const name = row[3]?.toString().trim();
    const comps = row[4]?.toString().trim();

    if (!nodeId || !name) continue;

    if (!map[nodeId]) {
      map[nodeId] = {
        id: nodeId,
        parent: parentId || null,
        name: name,
        comps: comps || "",
      };
    }
  }

  return Object.values(map);
}

function include(file) {
  return HtmlService.createHtmlOutputFromFile(file).getContent();
}

コードの配置は以上です。

画面右上の「デプロイ」から「新しいデプロイ」を開き、「ウェブアプリ」としてデプロイします。

発行されたURLにアクセスし、階層構造が正常に表示されるか確認します。

ウェブアプリとしてデプロイし、発行されたURLで正常に階層構造が表示された画面。

この記事で紹介しているコードは、GitHubのMOTOKI-LLC/cg-method-codeにもまとめています。

GASでシートを操作する記事は、ほかにもあります。

まとめ

  • D3.jsとスプレッドシートを組み合わせることで、シート編集のみで保守できる階層ビジュアライザーを構築できます。
  • 要件に合わせてカスタマイズすることで、組織図やUI構成図、カテゴリツリーなどの可視化に応用できます。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

スプレッドシート・GASのおすすめ書籍

作業環境の作り方

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次