Datenvisualisierung mit Claude Code: Revenue-Dashboard bauen
Baue Datenvisualisierung mit Claude Code: Recharts, D3, CSV-Aggregation, Accessibility und Playwright-Screenshots.
Erst die Entscheidung, dann das Diagramm
Wer Claude Code nur bittet, eine Datenvisualisierung zu bauen, bekommt oft eine hübsche Demo. Ein gutes Dashboard beginnt aber mit der Entscheidung, die es unterstützen soll. Für ClaudeCodeLab reichen Pageviews nicht aus. Wichtiger ist, ob Leser Artikel beenden, interne Links klicken, Produktseiten öffnen, Trainingsanfragen senden oder von einem kostenlosen Download zu einer bezahlten Vorlage wechseln.
In diesem Leitfaden entsteht ein kleines Dashboard für Content-Analytics und Monetarisierung mit React, TypeScript, Recharts, einem kleinen D3-Helfer, CSV-Aggregation, zugänglichen Labels, responsiven Zuständen und Playwright-Screenshots. Recharts ist die pragmatische Wahl für React-Komponenten. D3 ist eher ein Werkzeugkasten für Skalen, Transformationen und Spezialberechnungen. Für ein operatives Dashboard ist Recharts der Standard, D3 ergänzt nur gezielt.
Ein Datenvertrag ist die Absprache zwischen Analytics-Export und UI: date ist ein ISO-Datum, sessions ist eine Zahl, revenue ist ein numerischer USD-Wert und channel kommt aus einer festen Liste. Aggregation bedeutet, Rohzeilen in entscheidbare Summaries wie Umsatz pro Kanal oder Conversion Rate zu verdichten. Accessibility heißt, dass ein Diagramm auch über Überschriften, Tabelle, Kontrast und Tastaturbedienung verständlich bleibt.
Passende interne Artikel sind Analytics Implementation, Dashboard Development und Accessibility. Offizielle Quellen: Claude Code docs, Recharts getting started, D3 getting started, Playwright screenshots und WCAG non-text contrast.
Konkrete Use Cases
| Use Case | Metriken | Diagramm | Aktion |
|---|---|---|---|
| Artikel verbessern | Lesefertigstellung, CTA-Klicks, SEO-Traffic | Linie und Balken | Intro, interne Links und CTA ändern |
| Produkte verkaufen | Produktklicks, Umsatz, Kanal-Mix | Balken und Anteil | Produktkarten, Preistext und Vergleich verbessern |
| Training und Beratung | Formularabschluss, Downloads, B2B-Traffic | KPI-Karten und Funnel | Weg zur Beratung klarer machen |
| AdSense-Qualität | Lesetiefe, Scrolltiefe, Ausstieg bei Anzeigen | Annotierter Trend | Leseerlebnis vor mehr Anzeigen schützen |
Die Diagrammauswahl sollte nüchtern bleiben. Zeitverlauf als Linie, Kategorienvergleich als Balken, Anteile nur bei wenigen Kategorien und wichtige Einzelwerte als KPI. Dual-Axis-Charts sind für solche Dashboards riskant, weil Umsatz und Conversion Rate durch die Achsen scheinbar zusammenhängen können.
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 für 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.
Dieser Prompt zwingt Claude Code dazu, Datenform, Fehlerfälle, Accessibility und Nachweis gleich mitzudenken.
Installation
npm i recharts d3
npm i -D @types/d3 @playwright/test
Datenvertrag und CSV-Aggregation
Fixiere die Datenform vor dem Diagramm. Sonst entstehen schnell erfundene Felder, Revenue als String oder NaN im Tooltip.
// 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);
}
Für einfache Exporte reicht das. Bei Quotes, Kommas in Feldern, lokalen Dezimalformaten oder Zeitzonen braucht es eine CSV-Library und echte Fixtures.
React-Dashboard
Das Dashboard enthält KPI-Karten, Chart-Tabs, loading/error/empty, ResponsiveContainer und eine Tabellen-Fallback-Ansicht. Die Tabelle ist wichtig, weil sie exakte Werte prüfbar macht.
// 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 als gezielter Helfer
// 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]),
};
}
Fehlerbilder
Das erste Risiko ist eine abgeschnittene Y-Achse im Balkendiagramm. Kleine Unterschiede wirken dann zu groß. Das zweite ist ungeprüftes CSV mit leeren Werten, falschen Daten oder unbekannten Kanälen. Das dritte ist Bedeutung nur über Farbe. Das vierte sind fehlende Loading-, Error- und Empty-States. Das fünfte ist fehlende Mobile-Prüfung bei 390px Breite.
Playwright-Screenshots
// 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
Screenshots beweisen nicht die Mathematik; dafür brauchst du Unit-Tests der Aggregation. Sie finden aber leere Charts, überlappende Labels, horizontales Overflow und fehlende Tabellen schnell.
An Umsatz anschließen
Datenvisualisierung ist kein Schmuck. Wenn Leser einen Artikel beenden, aber nicht zur Produktbibliothek wechseln, ist der CTA schwach. Wenn Beratungsartikel Traffic bekommen, aber keine Leads erzeugen, muss die Seite für Training und Beratung klarer werden. Für Anfänger kann die kostenlose Checkliste der nächste sinnvolle Schritt sein.
Beim Test dieses Musters war der größte Gewinn, den Datenvertrag vor dem Diagramm festzulegen. Claude Code liefert deutlich bessere Ergebnisse, wenn Empty-States, Tabellen-Fallback, Mobile-Screenshots und Schutz vor irreführenden Diagrammen von Anfang an gefordert werden. Echte CSV-Exports mit Quotes, Zeitzonen und Tool-spezifischen Feldern müssen trotzdem separat geprüft werden.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.