Use Cases (Mis à jour: 02/06/2026)

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.

Visualisation de données avec Claude Code : dashboard de revenus

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

CasMétriquesGraphiqueAction
Amélioration d’articlesLecture complète, clics CTA, trafic SEOCourbe et barresRéécrire l’intro, les liens et le CTA
Vente de produitsClics produit, revenus, mix canalBarres et répartitionAméliorer cartes produit, prix et comparaison
Formation et conseilFormulaire envoyé, téléchargements, trafic B2BKPI et entonnoirClarifier le chemin vers la consultation
Qualité AdSenseLecture, scroll, sorties près des annoncesTendance annotéePré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.

#Claude Code #visualisation de données #Recharts #D3 #dashboard #React
Gratuit

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.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.