Use Cases (Aktualisiert: 2.6.2026)

Datenvisualisierung mit Claude Code: Revenue-Dashboard bauen

Baue Datenvisualisierung mit Claude Code: Recharts, D3, CSV-Aggregation, Accessibility und Playwright-Screenshots.

Datenvisualisierung mit Claude Code: Revenue-Dashboard bauen

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 CaseMetrikenDiagrammAktion
Artikel verbessernLesefertigstellung, CTA-Klicks, SEO-TrafficLinie und BalkenIntro, interne Links und CTA ändern
Produkte verkaufenProduktklicks, Umsatz, Kanal-MixBalken und AnteilProduktkarten, Preistext und Vergleich verbessern
Training und BeratungFormularabschluss, Downloads, B2B-TrafficKPI-Karten und FunnelWeg zur Beratung klarer machen
AdSense-QualitätLesetiefe, Scrolltiefe, Ausstieg bei AnzeigenAnnotierter TrendLeseerlebnis 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.

#Claude Code #Datenvisualisierung #Recharts #D3 #Dashboard #React
Kostenlos

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.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.