Claude CodeでD3.jsデータ可視化を実装する実践ガイド
Claude CodeでD3.jsの棒グラフを実装する手順、実例、落とし穴、検証方法を初心者向けに解説。
Claude CodeとD3.jsを一緒に使う理由
D3.jsでチャートを作ろうとして最初につまずくのは、棒グラフの描き方そのものではありません。scaleBand、axisLeft、selection.join、transitionのような部品がどこでつながるのかを理解する前に、サンプルコードを切り貼りして壊してしまうことです。
Claude Codeはここで役に立ちます。既存のHTML、CSS、TypeScript、データ形式を読ませたうえで「このデータをどの軸に置くか」「レスポンシブにするか」「アクセシビリティをどう担保するか」まで一緒に設計できます。D3.jsはデータに応じてDOMやSVGを操作するライブラリなので、仕様が曖昧なまま実装すると、動くけれど保守しにくいチャートになりがちです。
この記事では、初心者でもコピペで動かせるVite + TypeScriptのD3.jsサンプルを使いながら、Claude Codeに何を頼むと失敗しにくいかを整理します。D3の公式情報はD3 Getting started、データ結合はJoining data、スケールはd3-scale、軸はd3-axis、アニメーションはd3-transitionを確認してください。
初心者向けに全体像を分解する
D3.jsの基本は「データをピクセルに変換し、SVG要素として描く」ことです。難しい言葉を最初に整理しておくと、Claude Codeへの指示も具体的になります。
| 部品 | 平易な説明 | 今回のコードでの役割 |
|---|---|---|
| selection | DOM要素を選ぶ操作 | #chartにsvgを追加する |
| scale | 数値やカテゴリを画面上の位置に変換するもの | チャネル名を横位置、CV数を縦位置に変換する |
| axis | 目盛りとラベル | 横軸にチャネル、縦軸にCV数を表示する |
| mark | 実際に見える図形 | rectで棒、pathで補助線を描く |
| join | データとDOM要素を対応させる処理 | データ数が変わっても棒を更新できる |
| transition | 変化をなめらかに見せる処理 | 棒が下から伸びるアニメーションを付ける |
Claude Codeには「D3で棒グラフを作って」だけでなく、データ構造、表示単位、アクセシビリティ、更新時の挙動、検証方法まで渡すのがコツです。たとえば次のように依頼します。
Vite + TypeScript + D3 v7で、流入チャネル別の訪問数とCV数を表示するレスポンシブ棒グラフを作ってください。
selection.joinを使い、ツールチップ、キーボードフォーカス、aria-label、空データ時の扱い、ブラウザコンソールで確認できるスモークテストも含めてください。
コピペで動くD3.js + TypeScriptサンプル
まずnpm create vite@latest d3-demo -- --template vanilla-tsで最小構成を作り、npm i d3 @types/d3を入れます。以下の4ファイルを置き換えれば、npm run devで確認できます。D3のバージョンは公式ドキュメントで案内されているv7系を前提にしています。
{
"scripts": {
"dev": "vite"
},
"dependencies": {
"d3": "^7.9.0"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"typescript": "latest",
"vite": "latest"
}
}
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>D3 Conversion Chart</title>
</head>
<body>
<main class="page">
<h1>D3.js Conversion Dashboard</h1>
<section class="chart-shell" aria-describedby="chart-summary">
<div id="chart"></div>
<p id="chart-summary" class="sr-only"></p>
</section>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
:root {
color: #172033;
background: #f7f7f3;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
}
.page {
width: min(920px, calc(100vw - 32px));
margin: 40px auto;
}
.chart-shell {
border: 1px solid #d8d5cc;
border-radius: 8px;
background: #ffffff;
padding: 20px;
}
#chart {
min-height: 320px;
position: relative;
}
#chart svg {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.axis-label {
fill: #475569;
font-size: 12px;
}
.bar {
fill: #2563eb;
outline: none;
}
.bar:hover,
.bar:focus {
fill: #dc2626;
}
.trend-line {
fill: none;
stroke: #0f172a;
stroke-width: 2;
pointer-events: none;
}
.chart-tooltip {
position: absolute;
top: 0;
left: 0;
max-width: 220px;
border-radius: 6px;
background: #172033;
color: #ffffff;
font-size: 13px;
line-height: 1.5;
opacity: 0;
padding: 8px 10px;
pointer-events: none;
transform: translate(-9999px, -9999px);
transition: opacity 120ms ease;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
import * as d3 from "d3";
import "./style.css";
type ChannelDatum = {
channel: string;
visitors: number;
conversions: number;
};
const data: ChannelDatum[] = [
{ channel: "Search", visitors: 4200, conversions: 168 },
{ channel: "Newsletter", visitors: 2600, conversions: 182 },
{ channel: "Social", visitors: 3100, conversions: 96 },
{ channel: "Partner", visitors: 1400, conversions: 84 },
];
const numberFormat = new Intl.NumberFormat(undefined);
const percentFormat = new Intl.NumberFormat(undefined, {
style: "percent",
maximumFractionDigits: 1,
});
function conversionRate(datum: ChannelDatum): number {
return datum.visitors === 0 ? 0 : datum.conversions / datum.visitors;
}
function drawConversionChart(container: HTMLElement, items: ChannelDatum[]): void {
container.replaceChildren();
if (items.length === 0) {
container.textContent = "No data to display.";
return;
}
const margin = { top: 28, right: 24, bottom: 56, left: 64 };
const outerWidth = 760;
const outerHeight = 420;
const width = outerWidth - margin.left - margin.right;
const height = outerHeight - margin.top - margin.bottom;
const svg = d3
.select(container)
.append("svg")
.attr("viewBox", `0 0 ${outerWidth} ${outerHeight}`)
.attr("role", "img")
.attr("aria-labelledby", "chart-title chart-desc");
svg.append("title").attr("id", "chart-title").text("Conversions by channel");
svg
.append("desc")
.attr("id", "chart-desc")
.text("Bar chart comparing conversions from each acquisition channel.");
const plot = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const x = d3
.scaleBand<string>()
.domain(items.map((d) => d.channel))
.range([0, width])
.padding(0.28);
const y = d3
.scaleLinear()
.domain([0, d3.max(items, (d) => d.conversions) ?? 0])
.nice()
.range([height, 0]);
plot
.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.call((axis) => axis.selectAll("text").attr("dy", "0.85em"));
plot.append("g").call(d3.axisLeft(y).ticks(5));
plot
.append("text")
.attr("class", "axis-label")
.attr("x", -margin.left + 4)
.attr("y", -10)
.text("Conversions");
const tooltip = d3.select(container).append("div").attr("class", "chart-tooltip");
function showTooltip(event: PointerEvent | FocusEvent, datum: ChannelDatum): void {
const xCenter = (x(datum.channel) ?? 0) + x.bandwidth() / 2 + margin.left;
const yTop = y(datum.conversions) + margin.top;
const [left, top] =
"clientX" in event ? d3.pointer(event, container) : [xCenter, yTop];
tooltip
.style("opacity", "1")
.style("transform", `translate(${left + 12}px, ${top - 28}px)`)
.html(
`<strong>${datum.channel}</strong><br />` +
`Visitors: ${numberFormat.format(datum.visitors)}<br />` +
`Conversions: ${numberFormat.format(datum.conversions)}<br />` +
`CVR: ${percentFormat.format(conversionRate(datum))}`,
);
}
function hideTooltip(): void {
tooltip.style("opacity", "0").style("transform", "translate(-9999px, -9999px)");
}
const bars = plot
.selectAll<SVGRectElement, ChannelDatum>("rect.bar")
.data(items, (d) => d.channel)
.join((enter) =>
enter
.append("rect")
.attr("class", "bar")
.attr("x", (d) => x(d.channel) ?? 0)
.attr("width", x.bandwidth())
.attr("y", height)
.attr("height", 0),
)
.attr("tabindex", 0)
.attr("role", "img")
.attr(
"aria-label",
(d) =>
`${d.channel}: ${numberFormat.format(d.conversions)} conversions, ${percentFormat.format(
conversionRate(d),
)} conversion rate`,
)
.on("pointerenter pointermove", showTooltip)
.on("focus", showTooltip)
.on("pointerleave blur", hideTooltip);
bars
.transition()
.duration(700)
.delay((_d, index) => index * 80)
.attr("x", (d) => x(d.channel) ?? 0)
.attr("width", x.bandwidth())
.attr("y", (d) => y(d.conversions))
.attr("height", (d) => height - y(d.conversions));
const trendLine = d3
.line<ChannelDatum>()
.x((d) => (x(d.channel) ?? 0) + x.bandwidth() / 2)
.y((d) => y(d.conversions))
.curve(d3.curveMonotoneX);
plot
.append("path")
.datum(items)
.attr("class", "trend-line")
.attr("d", trendLine);
}
const chart = document.querySelector<HTMLElement>("#chart");
if (!chart) {
throw new Error("Missing #chart element.");
}
drawConversionChart(chart, data);
const summary = document.querySelector<HTMLElement>("#chart-summary");
if (summary) {
const best = data.reduce((current, item) =>
conversionRate(item) > conversionRate(current) ? item : current,
);
summary.textContent = `Highest conversion rate: ${best.channel}, ${percentFormat.format(
conversionRate(best),
)}.`;
}
ブラウザで表示したあと、コンソールで最低限の確認もできます。
console.log(document.querySelectorAll("#chart rect.bar").length);
console.log(document.querySelector("#chart svg")?.getAttribute("role"));
console.log(document.querySelector("#chart-summary")?.textContent);
3つ以上の実務ユースケース
| ユースケース | D3.jsが向いている理由 | Claude Codeに頼むこと |
|---|---|---|
| コンテンツ導線のCV分析 | 流入元、記事カテゴリ、CTAクリックを同じ画面で比較できる | アナリティクス実装のイベント定義を読み、グラフの指標を揃える |
| SaaSの利用状況ダッシュボード | 機能利用回数、継続率、プラン別差分などを細かく表現できる | データ型、空状態、読み込み状態、差分表示をTypeScriptで固定する |
| エラー監視や運用レポート | 時系列、しきい値、異常値の強調を自由に設計できる | パフォーマンス最適化と合わせて、大量点数の描画負荷を確認する |
| A/Bテスト結果の可視化 | 信頼区間やセグメント別の差を、表だけより直感的に示せる | A/Bテスト実装の集計ロジックとチャート表示を分ける |
実務では、D3.jsを「きれいなグラフを描く道具」としてだけ見ないほうが成果につながります。問い合わせ、購入、登録、継続利用など、収益につながる行動を可視化して初めて、ダッシュボードが意思決定の道具になります。
よくある失敗と落とし穴
| 落とし穴 | 起きる問題 | 対策 |
|---|---|---|
| 既存SVGを消さずに再描画する | 画面遷移や再レンダーのたびに棒や軸が増える | container.replaceChildren()やクリーンアップ関数を最初に入れる |
scaleの責務を曖昧にする | 数値を直接ピクセルにしてレスポンシブ崩れが起きる | データ値は必ずscaleLinearやscaleBandを通す |
joinを使わずenterだけで描く | データ更新時に古い要素が残る | selection.data(...).join(...)を標準パターンにする |
| Tooltipをマウス専用にする | キーボード利用者やスクリーンリーダー利用者に情報が届かない | tabindex、aria-label、focusイベントを入れる |
| ReactやAstroの再レンダーと衝突する | D3が触ったDOMをフレームワーク側が上書きする | D3が触る領域を1つのコンテナに閉じ込める |
| 色だけで意味を伝える | 色覚特性や印刷時に差が分からない | ラベル、線種、数値、凡例を併用する。詳しくはアクセシビリティ対応を参照する |
Claude Codeにレビューさせるときは「バグを探して」では弱いです。「再描画時の重複、空配列、0除算、キーボード操作、スクリーンリーダー、モバイル幅、データ件数1000件時の負荷をレビューして」と観点を列挙すると、指摘の質が上がります。
SEOと収益導線まで含めて設計する
D3.jsの記事は検索流入を取りやすい一方で、読者がコードだけコピーして離脱しやすいテーマです。記事内では「何を可視化すべきか」「どの指標が売上に近いか」まで示し、関連するTypeScript Tips、SEO最適化、デザインシステムへ自然につなげると回遊が生まれます。
ClaudeCodeLabでは、Claude Codeのプロジェクト設定、レビュー用プロンプト、CLAUDE.mdテンプレート、実装相談をまとめて提供しています。D3.jsのように「コードは動くが、事業指標に結びつかない」領域こそ、テンプレート・教材やトレーニング相談でチームの実装ルールに落とし込む価値があります。
この記事で紹介した内容を実際に試した結果
このサンプル構成では、D3.jsの難所を「データ型」「スケール」「軸」「マーク」「インタラクション」に分けてClaude Codeへ渡すと、修正回数がかなり減りました。特に効果があったのは、最初からaria-labelとフォーカス時Tooltipを条件に入れたことです。後からアクセシビリティ対応を足すより、初回実装の段階で確認観点に入れたほうが、コードも記事も説明しやすくなります。
まとめると、Claude CodeとD3.jsの相性は「手早くグラフを出す」だけではありません。データの意味、表示の意図、検証観点、収益導線まで同じ仕様に閉じ込めることで、公開後に使われる可視化になります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。