この記事では、Googleスプレッドシートで管理している階層データを、D3.jsを使ってブラウザ上にツリー構造で可視化する方法を解説します。
表示イメージ
スプレッドシート
以下の画像は旧版の参考です。今回の修正版コードの実行画面ではありません。入力は後述の表を使い、予約管理のparentIdはsampleにします。

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

修正版はルート1件・見出しを除く100行以内の多階層データを扱います。複数の独立したツリーには対応していません。
また、ノードをホバーした際に補足情報をツールチップで表示する仕様にしています。
練習用の階層図を作る手順
作成の順番です。内容は、この記事の説明とコードにもとづく目安です。コードは、外部のライブラリ(D3.js)を読み込みます。ライブラリのバージョンや配布元は変わることがあるため、使う前に確認してください。コードは、doGetを使うため、ブラウザーで表示するには、Google Apps Scriptのウェブアプリとして公開する操作が必要です。公開の範囲(自分だけか、他の人もか)は、Googleの公式の案内を確認し、慎重に決めてください。
- 階層構造を定義するスプレッドシートを用意する。記事の構成例は、グループ名、nodeId、parentId、ノード名、補足情報の列。
- 親のノードのIDを、parentIdに入れて、階層を表す。記事では、多階層のデータ構造にも対応していると説明している。
- メニューの「拡張機能」→「Apps Script」でエディタを開く。
- 記事のコード全文を、index.html、css.html、js.html、main.gsの4つのファイルに貼り付けて保存する。
- main.gsのTREE_SHEET_IDに練習用シートのID、TREE_SHEET_NAMEにシート名を設定する。IDはURLの/d/と/editの間。
- ウェブアプリとして実行し、ブラウザー上にツリー構造で表示されるか確認する。ノードにカーソルを合わせると、補足情報がツールチップで表示される。
組織図などの情報を扱う場合は、ウェブアプリの公開範囲に注意してください。個人情報や社外秘の情報を、意図せず公開しないようにしてください。
スプレッドシートの作成
架空の情報だけを入れた練習用スプレッドシートを用意します。A1:E1は表の見出しと一致させ、2行目からデータを入力します。nodeIdは全行で一意、parentIdは既存のnodeIdに合わせ、ルートの1行だけ空欄にします。A列のグループ名は今回の描画では使いません。
構成例は次のとおりです。
| グループ名 | nodeId | parentId | ノード名 | 補足情報 |
|---|---|---|---|---|
| 猫カフェにゃんた | sample | 店舗本部 | 住所、責任者、営業時間 | |
| 猫カフェにゃんた | sample/猫管理 | sample | 猫管理エリア | 名前、年齢、性格 |
| 猫カフェにゃんた | sample/猫管理/ミケ | sample/猫管理 | ミケ | 3歳、甘えん坊、♀ |
| 猫カフェにゃんた | sample/猫管理/タマ | sample/猫管理 | タマ | 2歳、活発、♂ |
| 猫カフェにゃんた | sample/設備管理 | sample | 設備管理 | 空調、清掃、備品 |
| 猫カフェにゃんた | sample/予約管理 | sample | 予約・顧客管理 | 予約一覧、会員カード |
シートを作成したら、メニューの「拡張機能」から「Apps Script」を選択してエディタを開きます。

手順は次のとおりです。
GAS│コードの構成
index.htmlで<svg>を配置css.htmlにスタイル設定を記述js.htmlに D3.js による描画ロジックを記述main.gsでスプレッドシートのデータを取得

コード全文
各ファイルに以下のコードを貼り付けて保存します。
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件が表示されること、親子関係、補足情報、横スクロール、長い名前の省略を確認します。シート変更はページ再読み込みで反映します。コード変更は既存デプロイを新バージョンへ更新します。練習を終えるときはデプロイをアーカイブできます。

この記事で紹介しているコードは、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もページ上で実行されるので、社内情報での運用を確認済みとはしていません。
