Visualisation de données avec Claude Code : dashboard de revenus
Implémentez un dashboard utile avec Claude Code : Recharts, D3, CSV, accessibilité et captures Playwright.
Commencez par la décision, pas par le graphique
Demander à Claude Code de “faire une visualisation de données” donne souvent une belle démo, mais pas forcément un outil exploitable. Un bon dashboard commence par la décision à prendre. Pour ClaudeCodeLab, les pages vues ne suffisent pas. Il faut savoir si les lecteurs terminent les articles, cliquent sur les liens internes, ouvrent les pages produits, demandent une formation ou passent d’une ressource gratuite à un modèle payant.
Ce guide construit un petit dashboard d’analyse de contenu et de monétisation avec React, TypeScript, Recharts, un helper D3 limité, une agrégation CSV, des libellés accessibles, une mise en page responsive et des captures Playwright. Recharts est le choix pratique pour des composants de graphiques React. D3 est plutôt une boîte à outils de bas niveau pour les échelles, transformations et calculs particuliers. Dans ce cas, utilisez Recharts pour l’interface et D3 seulement pour les besoins précis.
Un contrat de données est l’accord entre la source analytique et l’UI : date est une date ISO, sessions un nombre, revenue un montant numérique en USD et channel une valeur connue. Agréger signifie transformer des lignes brutes en résumés actionnables, comme les revenus par canal ou le taux de conversion. Accessibilité signifie que le graphique reste compréhensible grâce aux titres, tableaux, contrastes et contrôles, sans dépendre uniquement de la couleur ou de la souris.
À lire avec l’implémentation analytics, le développement de dashboard et l’accessibilité. Références officielles : Claude Code docs, Recharts getting started, D3 getting started, Playwright screenshots et WCAG non-text contrast.
Cas d’usage concrets
| Cas | Métriques | Graphique | Action |
|---|---|---|---|
| Amélioration d’articles | Lecture complète, clics CTA, trafic SEO | Courbe et barres | Réécrire l’intro, les liens et le CTA |
| Vente de produits | Clics produit, revenus, mix canal | Barres et répartition | Améliorer cartes produit, prix et comparaison |
| Formation et conseil | Formulaire envoyé, téléchargements, trafic B2B | KPI et entonnoir | Clarifier le chemin vers la consultation |
| Qualité AdSense | Lecture, scroll, sorties près des annonces | Tendance annotée | Préserver l’expérience avant d’ajouter des annonces |
Gardez le choix du graphique simple : courbe pour le temps, barres pour comparer, répartition seulement avec peu de catégories, KPI pour les chiffres à lire immédiatement. Évitez les graphiques à double axe dans les dashboards débutants, car ils peuvent suggérer une relation entre revenus et conversion qui vient seulement de l’échelle.
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
Prompt pour 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.
Le prompt force Claude Code à produire un composant vérifiable, pas seulement un graphique décoratif.
Installation
npm i recharts d3
npm i -D @types/d3 @playwright/test
Contrat de données et agrégation CSV
Fixez les champs avant le graphique. Cela évite les noms inventés, les revenus traités comme du texte et les NaN dans les tooltips.
// 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);
}
Ce parseur vise les exports simples. Avec des champs entre guillemets, des virgules dans les colonnes, des décimales locales ou des fuseaux horaires mélangés, utilisez une vraie librairie CSV et des fixtures issues de vos exports.
Composant React
Le composant contient des KPI, des onglets de graphique, les états loading/error/empty, un conteneur responsive et un tableau de secours. Le tableau permet de vérifier les valeurs et rend l’information plus robuste pour les technologies d’assistance.
// 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 en complément
// 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]),
};
}
Pièges à vérifier
Le premier piège est l’axe Y tronqué sur un graphique en barres, qui exagère les écarts. Le deuxième est le CSV sale : dates invalides, cellules vides et canaux inconnus doivent être nettoyés. Le troisième est le sens porté uniquement par la couleur. Le quatrième est l’absence d’états loading, error et empty. Le cinquième est l’oubli du mobile, où les libellés et tableaux cassent souvent.
Vérification 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
Les captures ne prouvent pas les calculs ; gardez des tests unitaires pour l’agrégation. Elles repèrent vite les graphiques vides, libellés superposés, débordements horizontaux et tableaux manquants.
Relier la visualisation aux revenus
La visualisation doit guider les décisions. Si un lecteur termine un article sans visiter la bibliothèque de produits, le CTA doit être revu. Si un article de conseil reçoit du trafic sans formulaire, la page formation et conseil doit formuler une offre plus nette. Si les débutants lisent plusieurs guides, la checklist gratuite peut devenir l’étape suivante.
En testant ce modèle, le meilleur résultat est venu du contrat de données fixé avant le graphique. Quand le prompt exige les états vides, le tableau, les captures mobiles et la vérification des graphiques trompeurs, Claude Code produit une base beaucoup plus exploitable. Le risque restant dépend des vrais CSV : guillemets, fuseaux horaires et formats propres à chaque outil doivent être validés avec des exports réels.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.