Claude Codeでデータ可視化ダッシュボードを実装する実践ガイド
Claude Codeで収益につながるデータ可視化を実装。Recharts、D3、CSV集計、アクセシビリティ、Playwright確認まで解説。
まず「きれいなグラフ」ではなく意思決定を決める
Claude Codeに「データ可視化を作って」と頼むだけだと、見た目は整っているのに使えないグラフが出がちです。大切なのは、どの数字を見て、誰が、何を決めるのかを先に書くことです。ClaudeCodeLabなら、記事の読了率、CTAクリック、教材ページへの送客、相談フォーム完了、Gumroad商品へのクリックを同じ画面で見たいはずです。ページビューだけを折れ線にしても、収益化の改善にはつながりません。
この記事では、Claude Codeを使って「コンテンツ分析と収益化のための小さなダッシュボード」を実装します。RechartsはReactコンポーネントとして扱いやすいグラフライブラリ、D3は尺度や変換を細かく制御する道具箱、と考えると初心者にも分かりやすいです。最初はRechartsで実装し、必要なところだけD3を使うのが現実的です。
用語も先にそろえます。データ契約とは、画面が受け取るデータの約束です。dateはISO形式、sessionsは数値、revenueはUSDの数値、というように決めます。集計とは、細かい行データを「チャネル別売上」「CVR」のように見やすくまとめる処理です。アクセシビリティとは、色やマウス操作だけに頼らず、キーボード利用者やスクリーンリーダー利用者にも意味が伝わる状態にすることです。
関連する内部記事として、計測設計はClaude Code Analytics Implementation、画面全体の作り方はダッシュボード開発、読みやすさはアクセシビリティ実装と合わせて確認してください。公式情報はClaude Code docs、Recharts getting started、D3 getting started、Playwright screenshots、WCAG 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本流して確認する必要があります。
無料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/相談導線の実務ルール。