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

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

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

表示イメージ

スプレッドシート

以下の画像は旧版の参考です。今回の修正版コードの実行画面ではありません。入力は後述の表を使い、予約管理のparentIdはsampleにします。

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

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

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

修正版はルート1件・見出しを除く100行以内の多階層データを扱います。複数の独立したツリーには対応していません。

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

目次

練習用の階層図を作る手順

作成の順番です。内容は、この記事の説明とコードにもとづく目安です。コードは、外部のライブラリ(D3.js)を読み込みます。ライブラリのバージョンや配布元は変わることがあるため、使う前に確認してください。コードは、doGetを使うため、ブラウザーで表示するには、Google Apps Scriptのウェブアプリとして公開する操作が必要です。公開の範囲(自分だけか、他の人もか)は、Googleの公式の案内を確認し、慎重に決めてください。

  1. 階層構造を定義するスプレッドシートを用意する。記事の構成例は、グループ名、nodeId、parentId、ノード名、補足情報の列。
  2. 親のノードのIDを、parentIdに入れて、階層を表す。記事では、多階層のデータ構造にも対応していると説明している。
  3. メニューの「拡張機能」→「Apps Script」でエディタを開く。
  4. 記事のコード全文を、index.html、css.html、js.html、main.gsの4つのファイルに貼り付けて保存する。
  5. main.gsのTREE_SHEET_IDに練習用シートのID、TREE_SHEET_NAMEにシート名を設定する。IDはURLの/d/と/editの間。
  6. ウェブアプリとして実行し、ブラウザー上にツリー構造で表示されるか確認する。ノードにカーソルを合わせると、補足情報がツールチップで表示される。

組織図などの情報を扱う場合は、ウェブアプリの公開範囲に注意してください。個人情報や社外秘の情報を、意図せず公開しないようにしてください。

スポンサーリンク

スプレッドシートの作成

架空の情報だけを入れた練習用スプレッドシートを用意します。A1:E1は表の見出しと一致させ、2行目からデータを入力します。nodeIdは全行で一意、parentIdは既存のnodeIdに合わせ、ルートの1行だけ空欄にします。A列のグループ名は今回の描画では使いません。

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

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

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

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

手順は次のとおりです。

GAS│コードの構成

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

コード全文

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

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

index.html

<!doctype html>
<html lang="ja"><head><base target="_top"><meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.min.js"></script>
<?!= include('css'); ?>
</head><body><p id="status" role="status">読み込み中</p>
<div id="container"><svg aria-label="階層図"></svg></div>
<p>補足情報:<span id="detail">ノードを選択してください</span></p>
<?!= include('js'); ?>
</body></html>

css.html

<style>
body {font-family:sans-serif;margin:16px;}
#container {overflow:auto;max-width:100%;}
svg {display:block;}
.node {cursor:pointer;}
.node:focus rect {stroke:#1264a3;stroke-width:3;}
#detail {white-space:pre-wrap;overflow-wrap:anywhere;}
</style>

js.html

<script>
const status = document.getElementById('status');
function showFailure(error) {
  status.textContent = '表示できません:' + (error.message || String(error));
}
function drawTree(nodes) {
  try {
    const root = d3.stratify().id(d => d.id).parentId(d => d.parent || null)(nodes);
    d3.tree().nodeSize([64, 240])(root);
    const all = root.descendants();
    const min = Math.min(...all.map(d => d.x));
    const max = Math.max(...all.map(d => d.x));
    const svg = d3.select('svg').attr('width', (root.height + 1) * 240 + 40).attr('height', max - min + 90);
    svg.selectAll('*').remove();
    const g = svg.append('g').attr('transform', `translate(20,${35-min})`);
    g.selectAll('path').data(root.links()).join('path')
      .attr('fill','none').attr('stroke','#777')
      .attr('d',d => `M${d.source.y+190},${d.source.x}H${d.source.y+215}V${d.target.x}H${d.target.y}`);
    const n = g.selectAll('.node').data(all).join('g').attr('class','node')
      .attr('transform',d => `translate(${d.y},${d.x})`).attr('tabindex',0).attr('role','button')
      .attr('aria-label',d => d.data.name + ':' + d.data.comps);
    const show = (event,d) => { document.getElementById('detail').textContent = d.data.name + ':' + (d.data.comps || '補足情報なし'); };
    n.on('mouseenter',show).on('focus',show).on('click',show)
      .on('keydown',(event,d) => {if (event.key==='Enter' || event.key===' ') {event.preventDefault();show(event,d);}});
    n.append('rect').attr('y',-18).attr('width',190).attr('height',36).attr('fill','#fff').attr('stroke','#333');
    n.append('text').attr('x',8).attr('y',5).attr('font-size',13)
      .text(d => {const a=Array.from(d.data.name);return a.length>12?a.slice(0,12).join('')+'…':d.data.name;});
    n.append('title').text(d => d.data.name + ':' + d.data.comps);
    status.textContent = nodes.length + '件を表示。シート変更後はページを再読み込みしてください。';
  } catch (error) {showFailure(error);}
}
if (typeof d3 === 'undefined') showFailure(new Error('D3.jsを読み込めませんでした'));
else google.script.run.withSuccessHandler(drawTree).withFailureHandler(showFailure).getPrefabTreeData();
</script>

main.gs

const TREE_SHEET_ID = 'COPY_SPREADSHEET_ID';
const TREE_SHEET_NAME = 'シート1';
function doGet() {
  return HtmlService.createTemplateFromFile('index').evaluate()
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
    .setTitle('階層ビュー');
}
function include(file) {
  if (!['css', 'js'].includes(file)) throw new Error('不明なファイル');
  return HtmlService.createHtmlOutputFromFile(file).getContent();
}
function getPrefabTreeData() {
  const sheet = SpreadsheetApp.openById(TREE_SHEET_ID).getSheetByName(TREE_SHEET_NAME);
  if (!sheet) throw new Error('指定したシートがありません');
  const last = sheet.getLastRow();
  if (last < 2 || last > 101) throw new Error('見出しを除き1〜100行にしてください');
  return validateTree_(sheet.getRange(1, 1, last, 5).getDisplayValues());
}
function validateTree_(data) {
  const headers = ['グループ名', 'nodeId', 'parentId', 'ノード名', '補足情報'];
  if (headers.some((h,i) => data[0][i].trim() !== h)) throw new Error('A1:E1の見出しを確認してください');
  const rows = data.slice(1).filter(r => r.some(v => v.trim()));
  if (!rows.length) throw new Error('データがありません');
  const nodes = rows.map(r => ({id:r[1].trim(), parent:r[2].trim(), name:r[3].trim(), comps:r[4]}));
  const map = new Map();
  for (const n of nodes) {
    if (!n.id || !n.name || n.id.length > 200 || n.parent.length > 200 || n.name.length > 80 || n.comps.length > 1000) throw new Error('ID・名前の空欄または文字数上限を確認してください');
    if (map.has(n.id)) throw new Error('nodeIdが重複しています');
    map.set(n.id, n);
  }
  for (const n of nodes) {
    const seen = new Set();
    let cur = n;
    while (cur) {
      if (seen.has(cur.id)) throw new Error('親子関係が循環しています');
      seen.add(cur.id);
      if (cur.parent && !map.has(cur.parent)) throw new Error('存在しないparentIdがあります');
      cur = cur.parent ? map.get(cur.parent) : null;
    }
  }
  if (nodes.filter(n => !n.parent).length !== 1) throw new Error('parentIdが空のルートは1件にしてください');
  return nodes;
}

画面幅とページタイトルはmain.gsのdoGetで設定します。HtmlOutputの公式仕様では、HTMLファイル内のmetaタグとtitle要素は無視されます。viewportはaddMetaTag、タイトルはsetTitleを使います。

画面右上の「デプロイ」から「新しいデプロイ」→「ウェブアプリ」を選び、実行ユーザーは自分、アクセスできるユーザーも自分だけにします。その選択肢がない環境では公開範囲を広げて代用せず、管理者の制限を確認してください。シートの「ウェブに公開」やリンク共有の拡大は不要です。

発行された/execのURLを開き、例の6件が表示されること、親子関係、補足情報、横スクロール、長い名前の省略を確認します。シート変更はページ再読み込みで反映します。コード変更は既存デプロイを新バージョンへ更新します。練習を終えるときはデプロイをアーカイブできます。

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

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

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

データ取得にはgoogle.script.runを使い、描画にはD3のstratifyを使います。従来の公開シート取得URLは使いません。重複ID・親IDの欠落・循環・複数ルートはエラーとして表示します。IDと親IDは各200文字、名前は80文字、補足は1000文字までです。

2026年10月28日に、入力検証とHtmlOutputへの設定をローカル模擬テストで確認し、JavaScript構文も検査しました。Apps Scriptでの権限承認・デプロイ・シート取得、D3の実描画、スマホ・キーボード操作は未確認です。既存画像は旧版の参考であり、修正版の実機確認の証拠ではありません。GitHub側の旧コードと差がある場合は、この本文の4ファイルをセットで使用してください。

スポンサーリンク

まとめ

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

よくある質問:スプレッドシートの階層をツリー表示するときのチェックリスト

この記事で、何ができますか?

Googleスプレッドシートで管理している階層データを、D3.jsを使って、ブラウザ上にツリー構造で可視化できます。ルート1件、見出しを除く100行以内の多階層に対応します。補足情報はマウス・キーボードのフォーカス・クリックで表示します。実ブラウザーでの表示確認は未実施です。

スプレッドシートは、どう作りますか?

階層構造を定義するスプレッドシートを用意します。記事の構成例は、グループ名、nodeId、parentId、ノード名、補足情報の列です。parentIdに親のノードのIDを入れて、階層を表します。

コードは、どう構成されていますか?

index.htmlでsvgを配置し、css.htmlにスタイル設定を記述し、js.htmlにD3.jsによる描画ロジックを記述し、main.gsでスプレッドシートのデータを取得します。

シート名が違うときは、どうしますか?

main.gsのTREE_SHEET_NAMEを自分のシート名に、TREE_SHEET_IDを練習用スプレッドシートのIDに変更します。

表示されないときは、何を確認しますか?

スプレッドシートのデータの列(nodeId、parentIdなど)と、シート名、コードの貼り付け先(4つのファイル)を確認します。コードは外部のD3.jsを読み込むため、ネットワークの接続やライブラリの読み込みも確認してください。

社内の組織図に使っても、安全ですか?

この練習は架空データと自分だけの公開範囲で行います。実行者の権限で読み取ったシート内容を画面へ返すため、アクセス範囲を広げるとシートの直接共有と異なる経路で情報が見える可能性があります。外部のD3.jsもページ上で実行されるので、社内情報での運用を確認済みとはしていません。

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

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

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

作業環境の作り方

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