Use Cases (更新: 2026/6/2)

Claude CodeでD3.jsデータ可視化を実装する実践ガイド

Claude CodeでD3.jsの棒グラフを実装する手順、実例、落とし穴、検証方法を初心者向けに解説。

Claude CodeでD3.jsデータ可視化を実装する実践ガイド

Claude CodeとD3.jsを一緒に使う理由

D3.jsでチャートを作ろうとして最初につまずくのは、棒グラフの描き方そのものではありません。scaleBandaxisLeftselection.jointransitionのような部品がどこでつながるのかを理解する前に、サンプルコードを切り貼りして壊してしまうことです。

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への指示も具体的になります。

部品平易な説明今回のコードでの役割
selectionDOM要素を選ぶ操作#chartsvgを追加する
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の責務を曖昧にする数値を直接ピクセルにしてレスポンシブ崩れが起きるデータ値は必ずscaleLinearscaleBandを通す
joinを使わずenterだけで描くデータ更新時に古い要素が残るselection.data(...).join(...)を標準パターンにする
Tooltipをマウス専用にするキーボード利用者やスクリーンリーダー利用者に情報が届かないtabindexaria-labelfocusイベントを入れる
ReactやAstroの再レンダーと衝突するD3が触ったDOMをフレームワーク側が上書きするD3が触る領域を1つのコンテナに閉じ込める
色だけで意味を伝える色覚特性や印刷時に差が分からないラベル、線種、数値、凡例を併用する。詳しくはアクセシビリティ対応を参照する

Claude Codeにレビューさせるときは「バグを探して」では弱いです。「再描画時の重複、空配列、0除算、キーボード操作、スクリーンリーダー、モバイル幅、データ件数1000件時の負荷をレビューして」と観点を列挙すると、指摘の質が上がります。

SEOと収益導線まで含めて設計する

D3.jsの記事は検索流入を取りやすい一方で、読者がコードだけコピーして離脱しやすいテーマです。記事内では「何を可視化すべきか」「どの指標が売上に近いか」まで示し、関連するTypeScript TipsSEO最適化デザインシステムへ自然につなげると回遊が生まれます。

ClaudeCodeLabでは、Claude Codeのプロジェクト設定、レビュー用プロンプト、CLAUDE.mdテンプレート、実装相談をまとめて提供しています。D3.jsのように「コードは動くが、事業指標に結びつかない」領域こそ、テンプレート・教材トレーニング相談でチームの実装ルールに落とし込む価値があります。

この記事で紹介した内容を実際に試した結果

このサンプル構成では、D3.jsの難所を「データ型」「スケール」「軸」「マーク」「インタラクション」に分けてClaude Codeへ渡すと、修正回数がかなり減りました。特に効果があったのは、最初からaria-labelとフォーカス時Tooltipを条件に入れたことです。後からアクセシビリティ対応を足すより、初回実装の段階で確認観点に入れたほうが、コードも記事も説明しやすくなります。

まとめると、Claude CodeとD3.jsの相性は「手早くグラフを出す」だけではありません。データの意味、表示の意図、検証観点、収益導線まで同じ仕様に閉じ込めることで、公開後に使われる可視化になります。

#Claude Code #D3.js #データ可視化 #チャート #フロントエンド
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。