アイデアが 100 個を超えたあたりから、「このアイデアと関連するものは何だっけ?」という問いに答えるのが難しくなってきました。リスト表示では、アイデア同士の関連性が見えない。
この課題を解決するため、IdeaSpool にネットワークグラフ表示機能を Claude Code で実装しました。アイデアをノード、関連性をエッジとして可視化することで、蓄積されたアイデアを俯瞰できるようになります。
この記事では、react-force-graph-2d を使った実装と、関連度計算の設計について解説します。
完成イメージ

84個のアイデアが881の接続で結ばれている様子。ノードをクリックすると、右パネルに関連アイデアが関連度とともに表示されます。黄色がアイデア、赤が課題・Pain Point、青がメモを表しています。
なぜグラフ可視化が必要だったのか
2026年1月29日の開発日記にはこう書いています:
視覚化機能: 蓄積されたアイデアを俯瞰する手段が必要。リスト表示だけでは関連性が見えない
アイデア管理ツールでは、日々のひらめきを記録していきます。数が増えるにつれて、以下の問題が出てきました:
- 埋もれるアイデア: 過去に記録したアイデアを忘れる
- 関連性が見えない: 「この問題、前に似たようなこと考えたな」と思っても探せない
- 全体像がわからない: 自分が何について考えているのか、傾向が見えない
ネットワークグラフなら、これらを解決できると考えました。関連するアイデアはクラスタになり、よく使うタグは大きなノードになる。視覚的に「自分のアイデアの地図」を描けます。
ライブラリ選定
可視化ライブラリの選定で悩みました。開発日記から:
- 視覚化ライブラリの選定
- ネットワーク: react-force-graph-2d(Canvas ベースで高パフォーマンス)
候補は以下でした:
| ライブラリ | 特徴 | 判断 |
|---|---|---|
| D3.js | 最も自由度が高い | 学習コストが高く、React との統合が面倒 |
| vis.js | 多機能 | バンドルサイズが大きい |
| react-force-graph-2d | Canvas ベース、React 統合が簡単 | 採用 |
| Cytoscape.js | グラフ理論に強い | オーバースペック |
react-force-graph-2d を選んだ理由:
- Canvas ベースで高パフォーマンス: 数百ノードでもスムーズに動作
- React との統合が簡単: コンポーネントとして使える
- Force Layout が組み込み: 物理シミュレーションを自前で書く必要がない
基本的な使い方
react-force-graph-2d の基本構造です:
import ForceGraph2D from 'react-force-graph-2d';
interface GraphData {
nodes: Array<{ id: string; name: string; }>;
links: Array<{ source: string; target: string; }>;
}
function NetworkGraph({ data }: { data: GraphData }) {
return (
<ForceGraph2D
graphData={data}
nodeLabel="name"
linkDirectionalParticles={2}
/>
);
}
graphData に渡すのは、nodes(ノード配列)と links(エッジ配列)です。links の source と target には、対応するノードの id を指定します。
アイデア間の関連度計算
グラフを描くには、アイデア間の「関連度」を計算する必要があります。開発日記から:
- タグベースの関連度計算
- Jaccard 係数を採用(共通タグ数 / タグ和集合数)
- 閾値の調整に試行錯誤
Jaccard 係数は、2 つの集合の類似度を測る指標です。0 から 1 の値を取り、1 に近いほど類似しています。
function jaccardSimilarity(tagsA: string[], tagsB: string[]): number {
const setA = new Set(tagsA);
const setB = new Set(tagsB);
// 共通要素(積集合)
const intersection = new Set(
[...setA].filter(tag => setB.has(tag))
);
// 全要素(和集合)
const union = new Set([...setA, ...setB]);
if (union.size === 0) return 0;
return intersection.size / union.size;
}
例えば:
- アイデア A のタグ:
["React", "TypeScript", "フロントエンド"] - アイデア B のタグ:
["React", "パフォーマンス"]
共通タグは ["React"](1 個)、和集合は ["React", "TypeScript", "フロントエンド", "パフォーマンス"](4 個)。
Jaccard 係数 = 1 / 4 = 0.25
閾値の調整
すべてのアイデアペアについてエッジを引くと、グラフが複雑になりすぎます。閾値を設けて、一定以上の関連度があるペアだけをエッジで結びます。
const SIMILARITY_THRESHOLD = 0.3;
function buildGraphData(ideas: Idea[]): GraphData {
// ノードを作成
const nodes = ideas.map(idea => ({
id: idea.id,
name: idea.title,
tags: idea.tags,
}));
// エッジを作成(閾値以上の関連度のみ)
const links: Link[] = [];
for (let i = 0; i < ideas.length; i++) {
for (let j = i + 1; j < ideas.length; j++) {
const similarity = jaccardSimilarity(
ideas[i].tags,
ideas[j].tags
);
if (similarity >= SIMILARITY_THRESHOLD) {
links.push({
source: ideas[i].id,
target: ideas[j].id,
value: similarity,
});
}
}
}
return { nodes, links };
}
閾値の調整は Claude Code と試行錯誤しながら進めました:
- 0.5 以上: エッジが少なすぎて、ほとんどのノードが孤立
- 0.2 以下: エッジが多すぎて、グラフが「毛玉」のように
- 0.3: ちょうど良いバランス
ただし、この閾値はタグの粒度によって変わります。タグが細かいほど共通タグが減るので、閾値を下げる必要があります。
ノードのカスタマイズ
ノードの見た目をカスタマイズして、情報を可視化します:
<ForceGraph2D
graphData={data}
nodeCanvasObject={(node, ctx, globalScale) => {
// ノードの大きさ(接続数に比例)
const size = Math.sqrt(node.connections || 1) * 4;
// タイプ別の色
const color = getColorByType(node.type);
// 円を描画
ctx.beginPath();
ctx.arc(node.x!, node.y!, size, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
// ラベルを描画(ズームレベルに応じて)
if (globalScale > 1.5) {
ctx.font = '4px Sans-Serif';
ctx.textAlign = 'center';
ctx.fillStyle = '#fff';
ctx.fillText(
node.name.slice(0, 10),
node.x!,
node.y! + size + 4
);
}
}}
/>
ポイント:
- ノードの大きさを接続数に比例: 多くのアイデアと関連するノードは大きく表示
- タイプ別の色分け: アイデアの種類(idea / pain-point / memo)を視覚的に区別
- ズームレベルに応じたラベル表示: ズームアウト時はラベルを省略してパフォーマンス向上
Force Layout のパラメータ調整
Force Layout(力学モデル)のパラメータ調整は奥が深いです。開発日記にも:
- Force Layout のパラメータ調整は奥が深い
主なパラメータ:
<ForceGraph2D
graphData={data}
// ノード間の反発力
d3VelocityDecay={0.3}
// リンクの長さ
linkDistance={100}
// 中心への引力
centerStrength={0.05}
// シミュレーションの冷却速度
cooldownTicks={100}
/>
調整のコツ:
- クラスタが重なる:
d3VelocityDecayを下げる(反発力を強める) - ノードが散らばりすぎ:
centerStrengthを上げる - 初期配置が安定しない:
cooldownTicksを増やす
パフォーマンス最適化
アイデアが 500 個を超えると、パフォーマンスが問題になります。最適化の工夫:
1. ノード数の制限
古いアイデアや関連度の低いアイデアを除外:
// 直近 6 ヶ月のアイデアのみ
const recentIdeas = ideas.filter(idea =>
new Date(idea.createdAt) > sixMonthsAgo
);
// 接続数が 0 のノードを除外
const connectedNodes = nodes.filter(node =>
links.some(link =>
link.source === node.id || link.target === node.id
)
);
2. エッジの間引き
関連度の高いエッジのみを残す:
// 関連度で降順ソートし、上位 N 件のみ残す
const topLinks = links
.sort((a, b) => b.value - a.value)
.slice(0, MAX_LINKS);
3. Canvas の最適化
react-force-graph-2d は Canvas ベースなので、DOM より高速です。ただし、以下に注意:
<ForceGraph2D
// WebGL を有効にする(対応ブラウザのみ)
enableNodeDrag={true}
// ズーム範囲を制限
minZoom={0.5}
maxZoom={10}
/>
インタラクション
グラフをクリックしたときの挙動を実装:
<ForceGraph2D
graphData={data}
onNodeClick={(node) => {
// アイデアの詳細を表示
setSelectedIdea(node.id);
}}
onNodeHover={(node) => {
// ホバー時にツールチップ
setHoveredNode(node);
}}
onLinkClick={(link) => {
// 関連度の詳細を表示
showRelationship(link.source, link.target);
}}
/>
ノードをクリックすると、そのアイデアの詳細と、関連するアイデアのリストを表示します。グラフでの俯瞰とリストでの詳細確認を行き来できるようにしました。
学んだこと
ネットワークグラフ可視化の実装を通じて学んだこと:
1. 関連度計算はシンプルに始める
最初は機械学習ベースの類似度計算を考えましたが、Jaccard 係数で十分でした。タグという明示的な分類情報があれば、複雑なアルゴリズムは不要です。
2. 閾値は実データで調整する
理論的な閾値より、実際のデータで試しながら調整するほうが良い結果になります。ユーザーのタグ付けの傾向によって最適な閾値は変わります。
3. パフォーマンスは最初から考慮する
「まず動くものを作って、あとで最適化」だと、後から大幅な設計変更が必要になります。ノード数・エッジ数の上限は最初から設計に組み込むべきでした。
4. Canvas ベースのライブラリを選ぶ
DOM ベースのライブラリ(SVG など)は、ノード数が増えるとパフォーマンスが急激に悪化します。大量のノードを扱う場合は、Canvas か WebGL ベースを選ぶべきです。
まとめ
react-force-graph-2d を使ったアイデアのネットワークグラフ可視化の実装ポイント:
- 関連度計算: Jaccard 係数でタグベースの類似度を計算
- 閾値設定: 0.3 程度を基準に、実データで調整
- ノードカスタマイズ: 接続数で大きさ、タイプで色を変える
- パフォーマンス: ノード数・エッジ数の上限を設ける
- インタラクション: クリック・ホバーで詳細表示
リスト表示だけでは見えなかったアイデア同士の関連性が、グラフで可視化されることで見えるようになります。「あ、この 2 つのアイデア、つながってたんだ」という発見があると、アイデア管理がより楽しくなります。
Zeronova(ゼロノバ)
Product Manager / AI-Native Builder
Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。