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

Claude Codeでデータ可視化ダッシュボードを実装する実践ガイド

Claude Codeで収益につながるデータ可視化を実装。Recharts、D3、CSV集計、アクセシビリティ、Playwright確認まで解説。

Claude Codeでデータ可視化ダッシュボードを実装する実践ガイド

まず「きれいなグラフ」ではなく意思決定を決める

Claude Codeに「データ可視化を作って」と頼むだけだと、見た目は整っているのに使えないグラフが出がちです。大切なのは、どの数字を見て、誰が、何を決めるのかを先に書くことです。ClaudeCodeLabなら、記事の読了率、CTAクリック、教材ページへの送客、相談フォーム完了、Gumroad商品へのクリックを同じ画面で見たいはずです。ページビューだけを折れ線にしても、収益化の改善にはつながりません。

この記事では、Claude Codeを使って「コンテンツ分析と収益化のための小さなダッシュボード」を実装します。RechartsはReactコンポーネントとして扱いやすいグラフライブラリ、D3は尺度や変換を細かく制御する道具箱、と考えると初心者にも分かりやすいです。最初はRechartsで実装し、必要なところだけD3を使うのが現実的です。

用語も先にそろえます。データ契約とは、画面が受け取るデータの約束です。dateはISO形式、sessionsは数値、revenueはUSDの数値、というように決めます。集計とは、細かい行データを「チャネル別売上」「CVR」のように見やすくまとめる処理です。アクセシビリティとは、色やマウス操作だけに頼らず、キーボード利用者やスクリーンリーダー利用者にも意味が伝わる状態にすることです。

関連する内部記事として、計測設計はClaude Code Analytics Implementation、画面全体の作り方はダッシュボード開発、読みやすさはアクセシビリティ実装と合わせて確認してください。公式情報はClaude Code docsRecharts getting startedD3 getting startedPlaywright screenshotsWCAG 2.2 Non-text Contrastを基準にします。

実務で使える3つ以上のユースケース

ユースケース見るべき指標選ぶグラフ改善アクション
記事改善読了率、CTAクリック、検索流入折れ線、棒グラフ導入文、内部リンク、CTA位置を変える
商品販売商品クリック、購入導線、チャネル別売上棒グラフ、構成比商品カード、価格訴求、比較表を改善する
研修・相談相談フォーム完了、資料DL、法人流入KPIカード、ファネル研修ページへの導線を明確にする
広告・AdSense読了率、離脱、広告付近のクリック折れ線、注釈付き棒グラフ広告を増やす前に読者体験を守る

グラフの選び方は単純です。時間の変化は折れ線、カテゴリ比較は棒、全体に占める割合は円または積み上げ棒、ひと目で見たい数値はKPIカードです。円グラフはカテゴリが多いと読みにくいので、5件を超えるなら棒グラフを優先します。売上とCVRのように単位が違う指標を二軸で重ねると、相関があるように見えてしまうため、初心者向けの管理画面では分けて見せる方が安全です。

flowchart LR
  Raw["CSV / analytics events"]
  Contract["data contract"]
  Aggregate["aggregation"]
  Chart["Recharts dashboard"]
  Review["accessibility and Playwright check"]
  CTA["training / products / consultation CTA"]

  Raw --> Contract --> Aggregate --> Chart --> Review --> CTA

Claude Codeへの依頼文

Claude Codeには、完成形だけでなく制約と検証方法を渡します。次の依頼文をそのまま使えます。

Build a React + TypeScript content analytics dashboard for ClaudeCodeLab.
Use Recharts for the main charts and a small D3 helper only for scale calculations.
Input data must follow a documented data contract: date, channel, sessions, signups, revenue, readRate.
Include sample data, CSV parsing, channel aggregation, loading state, error state, empty state, accessible labels, a table fallback, and responsive layout.
Avoid misleading charts: no truncated bar axis, no dual-axis chart, no color-only meaning.
Add Playwright screenshot checks for mobile and desktop.
Return copy-pasteable code and explain the failure modes to review.

この依頼文の狙いは、Claude Codeに「きれいなデモ」ではなく「公開前に壊れにくい実装」を作らせることです。特にデータ契約、空データ、エラー表示、アクセシビリティ、スクリーンショット確認を先に書くと、あとから手戻りしにくくなります。

インストール

Recharts中心なら依存関係は小さくできます。D3は全部を使うのではなく、必要な関数だけimportします。

npm i recharts d3
npm i -D @types/d3 @playwright/test

データ契約とCSV集計

まず画面に渡すデータの形を固定します。ここを曖昧にすると、Claude Codeがrevenueを文字列として扱ったり、日付の並び順を間違えたり、空欄をNaNとしてグラフに流したりします。

// dashboard-data.ts
export type Channel = "organic" | "email" | "social" | "referral" | "paid";

export type TrafficRow = {
  date: string;
  channel: Channel;
  sessions: number;
  signups: number;
  revenue: number;
  readRate: number;
};

export type ChannelSummary = {
  channel: Channel;
  sessions: number;
  signups: number;
  revenue: number;
  conversionRate: number;
  arps: number;
};

const channels: Channel[] = ["organic", "email", "social", "referral", "paid"];

export const sampleRows: TrafficRow[] = [
  { date: "2026-05-01", channel: "organic", sessions: 1280, signups: 42, revenue: 840, readRate: 0.61 },
  { date: "2026-05-01", channel: "email", sessions: 420, signups: 31, revenue: 1240, readRate: 0.74 },
  { date: "2026-05-01", channel: "social", sessions: 680, signups: 18, revenue: 260, readRate: 0.49 },
  { date: "2026-05-02", channel: "organic", sessions: 1360, signups: 48, revenue: 980, readRate: 0.64 },
  { date: "2026-05-02", channel: "referral", sessions: 310, signups: 17, revenue: 510, readRate: 0.58 },
  { date: "2026-05-02", channel: "paid", sessions: 540, signups: 22, revenue: 730, readRate: 0.52 },
];

function toNumber(value: string | undefined, fallback = 0) {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
}

function toChannel(value: string | undefined): Channel {
  return channels.includes(value as Channel) ? (value as Channel) : "referral";
}

export function parseAnalyticsCsv(csv: string): TrafficRow[] {
  const [headerLine, ...lines] = csv.trim().split(/\r?\n/);
  if (!headerLine) return [];

  const headers = headerLine.split(",").map((header) => header.trim());

  return lines
    .filter(Boolean)
    .map((line) => {
      const columns = line.split(",").map((column) => column.trim());
      const get = (name: string) => columns[headers.indexOf(name)];

      return {
        date: get("date") ?? "",
        channel: toChannel(get("channel")),
        sessions: toNumber(get("sessions")),
        signups: toNumber(get("signups")),
        revenue: toNumber(get("revenue")),
        readRate: toNumber(get("readRate")),
      };
    })
    .filter((row) => row.date && !Number.isNaN(Date.parse(row.date)));
}

export function aggregateByChannel(rows: TrafficRow[]): ChannelSummary[] {
  const map = new Map<Channel, ChannelSummary>();

  for (const row of rows) {
    const current =
      map.get(row.channel) ??
      { channel: row.channel, sessions: 0, signups: 0, revenue: 0, conversionRate: 0, arps: 0 };

    current.sessions += row.sessions;
    current.signups += row.signups;
    current.revenue += row.revenue;
    map.set(row.channel, current);
  }

  return [...map.values()]
    .map((row) => ({
      ...row,
      conversionRate: row.sessions === 0 ? 0 : row.signups / row.sessions,
      arps: row.sessions === 0 ? 0 : row.revenue / row.sessions,
    }))
    .sort((a, b) => b.revenue - a.revenue);
}

このCSVパーサーは、Google AnalyticsやPlausibleから出した単純なCSVを想定しています。引用符つきCSV、カンマを含む列、タイムゾーン変換が必要な場合は、Papa Parseのような専用パーサーを使う判断もClaude Codeにさせてください。

小さな分析ダッシュボードを実装する

次は実際のReactコンポーネントです。KPIカード、棒グラフ、折れ線、構成比、表を同じコンポーネントに入れています。表を残すのは、スクリーンリーダーでも数値を確認でき、スクリーンショットだけでは見落とす丸め誤差もレビューしやすいからです。

// ContentAnalyticsDashboard.tsx
"use client";

import { useMemo, useState } from "react";
import {
  Bar,
  BarChart,
  CartesianGrid,
  Cell,
  Legend,
  Pie,
  PieChart,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts";
import { aggregateByChannel, sampleRows, type Channel, type TrafficRow } from "./dashboard-data";

type DashboardProps = {
  rows?: TrafficRow[];
  isLoading?: boolean;
  error?: string | null;
};

type ViewMode = "revenue" | "conversion" | "mix";

const colors: Record<Channel, string> = {
  organic: "#2563eb",
  email: "#16a34a",
  social: "#f97316",
  referral: "#7c3aed",
  paid: "#dc2626",
};

const money = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

const percent = new Intl.NumberFormat("en-US", {
  style: "percent",
  maximumFractionDigits: 1,
});

export function ContentAnalyticsDashboard({
  rows = sampleRows,
  isLoading = false,
  error = null,
}: DashboardProps) {
  const [view, setView] = useState<ViewMode>("revenue");
  const summary = useMemo(() => aggregateByChannel(rows), [rows]);

  const totals = useMemo(
    () =>
      summary.reduce(
        (total, row) => ({
          sessions: total.sessions + row.sessions,
          signups: total.signups + row.signups,
          revenue: total.revenue + row.revenue,
        }),
        { sessions: 0, signups: 0, revenue: 0 },
      ),
    [summary],
  );

  if (isLoading) {
    return <p role="status">Loading dashboard data...</p>;
  }

  if (error) {
    return <p role="alert">Dashboard data could not be loaded: {error}</p>;
  }

  if (summary.length === 0) {
    return <p role="status">No data matches this date range. Try widening the filters.</p>;
  }

  const totalConversion = totals.sessions === 0 ? 0 : totals.signups / totals.sessions;

  return (
    <section aria-labelledby="content-analytics-title" style={{ display: "grid", gap: 24 }}>
      <header>
        <p style={{ margin: 0, color: "#64748b" }}>ClaudeCodeLab revenue dashboard</p>
        <h2 id="content-analytics-title" style={{ margin: "4px 0 0" }}>
          Content analytics dashboard
        </h2>
      </header>

      <div style={{ display: "grid", gap: 16, gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))" }}>
        <MetricCard label="Sessions" value={totals.sessions.toLocaleString("en-US")} />
        <MetricCard label="Signups" value={totals.signups.toLocaleString("en-US")} />
        <MetricCard label="Revenue" value={money.format(totals.revenue)} />
        <MetricCard label="Conversion" value={percent.format(totalConversion)} />
      </div>

      <div role="tablist" aria-label="Chart view" style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
        {[
          ["revenue", "Revenue by channel"],
          ["conversion", "Conversion rate"],
          ["mix", "Revenue mix"],
        ].map(([key, label]) => (
          <button
            key={key}
            type="button"
            role="tab"
            aria-selected={view === key}
            onClick={() => setView(key as ViewMode)}
            style={{
              border: "1px solid #cbd5e1",
              borderRadius: 8,
              padding: "8px 12px",
              background: view === key ? "#0f172a" : "#ffffff",
              color: view === key ? "#ffffff" : "#0f172a",
            }}
          >
            {label}
          </button>
        ))}
      </div>

      <figure aria-labelledby="dashboard-chart-title" style={{ margin: 0 }}>
        <h3 id="dashboard-chart-title" style={{ margin: "0 0 12px" }}>
          Dashboard chart
        </h3>
        <ResponsiveContainer width="100%" height={320}>
          {view === "revenue" ? (
            <BarChart data={summary} margin={{ top: 8, right: 24, bottom: 8, left: 8 }}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="channel" />
              <YAxis tickFormatter={(value) => money.format(Number(value))} domain={[0, "dataMax"]} />
              <Tooltip formatter={(value) => money.format(Number(value))} />
              <Bar dataKey="revenue" name="Revenue">
                {summary.map((row) => (
                  <Cell key={row.channel} fill={colors[row.channel]} />
                ))}
              </Bar>
            </BarChart>
          ) : view === "conversion" ? (
            <BarChart data={summary} margin={{ top: 8, right: 24, bottom: 8, left: 8 }}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="channel" />
              <YAxis tickFormatter={(value) => percent.format(Number(value))} domain={[0, "dataMax"]} />
              <Tooltip formatter={(value) => percent.format(Number(value))} />
              <Bar dataKey="conversionRate" name="Conversion rate">
                {summary.map((row) => <Cell key={row.channel} fill={colors[row.channel]} />)}
              </Bar>
            </BarChart>
          ) : (
            <PieChart>
              <Pie data={summary} dataKey="revenue" nameKey="channel" outerRadius={110} label>
                {summary.map((row) => (
                  <Cell key={row.channel} fill={colors[row.channel]} />
                ))}
              </Pie>
              <Tooltip formatter={(value) => money.format(Number(value))} />
              <Legend />
            </PieChart>
          )}
        </ResponsiveContainer>
        <figcaption style={{ color: "#64748b" }}>
          Bars start at zero, values are available in the table, and color is never the only label.
        </figcaption>
      </figure>

      <table aria-label="Channel summary" style={{ borderCollapse: "collapse", width: "100%" }}>
        <thead>
          <tr>
            {["Channel", "Sessions", "Signups", "Revenue", "CVR", "Revenue/session"].map((heading) => (
              <th key={heading} scope="col" style={{ borderBottom: "1px solid #cbd5e1", textAlign: "left", padding: 8 }}>
                {heading}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {summary.map((row) => (
            <tr key={row.channel}>
              <th scope="row" style={{ borderBottom: "1px solid #e2e8f0", textAlign: "left", padding: 8 }}>
                {row.channel}
              </th>
              <td style={{ borderBottom: "1px solid #e2e8f0", padding: 8 }}>{row.sessions.toLocaleString("en-US")}</td>
              <td style={{ borderBottom: "1px solid #e2e8f0", padding: 8 }}>{row.signups.toLocaleString("en-US")}</td>
              <td style={{ borderBottom: "1px solid #e2e8f0", padding: 8 }}>{money.format(row.revenue)}</td>
              <td style={{ borderBottom: "1px solid #e2e8f0", padding: 8 }}>{percent.format(row.conversionRate)}</td>
              <td style={{ borderBottom: "1px solid #e2e8f0", padding: 8 }}>{money.format(row.arps)}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </section>
  );
}

function MetricCard({ label, value }: { label: string; value: string }) {
  return (
    <div style={{ border: "1px solid #cbd5e1", borderRadius: 8, padding: 16 }}>
      <p style={{ margin: 0, color: "#64748b" }}>{label}</p>
      <strong style={{ display: "block", fontSize: 24, marginTop: 4 }}>{value}</strong>
    </div>
  );
}

D3は「全部D3化」ではなく補助に使う

D3は自由度が高い一方、Reactの状態管理やアクセシビリティまで自前で抱えると実装量が増えます。Claude Codeには、まずRechartsで画面を作り、独自のスケール計算や特殊な注釈だけD3で補助するように依頼すると安定します。

// d3-scales.ts
import { extent, scaleLinear, scaleTime } from "d3";

export function buildRevenueScales(
  rows: Array<{ date: string; revenue: number }>,
  width: number,
  height: number,
) {
  const dates = rows.map((row) => new Date(row.date)).filter((date) => !Number.isNaN(date.valueOf()));
  const revenues = rows.map((row) => row.revenue).filter((value) => Number.isFinite(value));
  const dateExtent = extent(dates);
  const revenueExtent = extent(revenues);

  const minDate = dateExtent[0] ?? new Date("2026-01-01");
  const maxDate = dateExtent[1] ?? minDate;
  const maxRevenue = Math.max(revenueExtent[1] ?? 0, 1);

  return {
    x: scaleTime().domain([minDate, maxDate]).range([0, width]),
    y: scaleLinear().domain([0, maxRevenue]).nice().range([height, 0]),
  };
}

失敗例と落とし穴

最も多い失敗は、棒グラフのY軸をゼロから始めないことです。差が大きく見え、広告やCTAの改善判断を誤ります。折れ線ではゼロ始まりが常に必要ではありませんが、棒グラフでは原則ゼロ始まりにしてください。

次に、CSVの空欄や文字列をそのままグラフへ渡す失敗です。NaNが混ざるとツールチップだけ壊れることもあり、レビューで見落とされます。集計前に数値変換、日付検証、カテゴリの既定値を入れるべきです。

3つ目は、色だけで意味を伝えることです。青がorganic、緑がemailだとしても、凡例、表、テキストラベルが必要です。WCAGの非テキストコントラストでは、意味を持つ図形にも十分なコントラストが求められます。

4つ目は、レスポンシブ確認不足です。PCではきれいでも、390px幅でラベルが重なったり、表が横にはみ出したりします。Claude Codeには「モバイルとデスクトップのスクリーンショットを比較する」と明示してください。

5つ目は、読み込み中、エラー、空データを作らないことです。ダッシュボードはAPIやCSVに依存するので、成功時だけの画面では本番運用に耐えません。

Playwrightでスクリーンショット確認する

Playwrightは、画面が実際に表示できているかを確認するために使います。ここではモバイルとデスクトップのスクリーンショットを保存し、見出し、図、表が存在することを確認します。

// tests/content-analytics-dashboard.spec.ts
import { expect, test } from "@playwright/test";

test.describe("content analytics dashboard", () => {
  for (const viewport of [
    { width: 390, height: 844 },
    { width: 1280, height: 900 },
  ]) {
    test(`renders at ${viewport.width}px`, async ({ page }) => {
      await page.setViewportSize(viewport);
      await page.goto("/analytics-demo");

      await expect(page.getByRole("heading", { name: /Content analytics dashboard/i })).toBeVisible();
      await expect(page.getByRole("figure", { name: /Dashboard chart/i })).toBeVisible();
      await expect(page.getByRole("table", { name: /Channel summary/i })).toBeVisible();
      await expect(page).toHaveScreenshot(`content-analytics-${viewport.width}.png`, {
        fullPage: true,
        animations: "disabled",
      });
    });
  }
});

実行は次の通りです。

npx playwright test tests/content-analytics-dashboard.spec.ts

スクリーンショットは万能ではありません。数値の正しさはユニットテスト、キーボード操作は手動確認、色のコントラストはアクセシビリティチェックで補います。ただし、ラベルの重なり、空のグラフ、表のはみ出しはかなり早く見つけられます。

ClaudeCodeLabの収益導線に組み込む

データ可視化は飾りではありません。記事のどこからプロダクト一覧に進むのか、どの導線がClaude Code研修・導入相談につながるのか、無料チートシートのthanksページが入口として機能しているのかを判断するための道具です。

Claude Codeにダッシュボードを作らせるときは、画面だけでなく、イベント名、CSV出力、レビュー観点、Playwright証跡まで同じ作業に含めてください。実装相談を受ける側としても、グラフの見た目より「どの意思決定に使う数字か」が明確なプロジェクトほど、改善速度が上がります。

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

今回のサンプルは、CSV行を型で固定し、チャネル別に集計し、Rechartsで表示し、D3はスケール補助に限定する流れで組みました。Claude Codeに最初から「空データ、エラー、表、モバイルスクリーンショット」を要求すると、単なるグラフ部品ではなくレビュー可能なダッシュボードに近づきます。一方、CSVの引用符対応や実データのタイムゾーン処理はプロジェクト差が大きいため、公開前に実データを1本流して確認する必要があります。

#Claude Code #データ可視化 #Recharts #D3 #ダッシュボード #React
無料

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

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

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

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

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

Masa

この記事を書いた人

Masa

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

PR

関連書籍・参考図書

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

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