D3.js-Datenvisualisierung mit Claude Code implementieren
D3.js mit Claude Code umsetzen: ausführbarer TypeScript-Code, Use Cases, Stolperfallen, Prüfung und Revenue-CTA.
Warum Claude Code bei D3.js hilft
Bei D3.js ist nicht das erste Rechteck der schwierige Teil. Schwierig wird es, wenn scaleBand, axisLeft, selection.join und transition zusammenpassen müssen und der Chart danach echte Daten, Responsive Layouts, Accessibility und Produktänderungen überstehen soll.
Claude Code kann vor dem Generieren den vorhandenen HTML-, CSS- und TypeScript-Code, Datenverträge und Projektkonventionen lesen. D3.js bindet Daten an DOM und SVG. Eine ungenaue Aufgabe erzeugt daher oft einen Chart, der einmal funktioniert, aber später schwer zu pflegen ist.
Nutze beim Anpassen die offiziellen Quellen: D3 Getting started, Joining data, d3-scale, d3-axis und d3-transition.
Einfaches Modell für Einsteiger
D3 übersetzt Daten in Pixel und SVG-Elemente. Wenn jede Schicht klar benannt ist, werden Prompts und Reviews präziser.
| Baustein | Einfache Erklärung | Rolle im Beispiel |
|---|---|---|
| selection | DOM-Elemente auswählen | Ein svg in #chart einfügen |
| scale | Datenwerte in Bildschirmpositionen umrechnen | Kanäle auf x, Conversions auf y abbilden |
| axis | Skalenstriche und Labels zeichnen | Kanäle und Conversion-Zahlen anzeigen |
| mark | Sichtbare Form | Balken mit rect, Hilfslinie mit path |
| join | Datenzeilen und DOM-Knoten verbinden | Balken bei Datenänderungen aktualisieren |
| transition | Visuelle Änderung animieren | Balken von unten wachsen lassen |
Ein guter Prompt enthält Datenform, Einheiten, Empty State, Tastaturbedienung und Prüfung.
Baue einen responsiven Balkenchart in Vite + TypeScript + D3 v7 für Besucher und Conversions je Akquisitionskanal. Nutze
selection.join, Tooltip, Tastaturfokus,aria-label, Empty-State-Handling und einen Browser-Konsolen-Smoke-Test.
Ausführbares D3.js + TypeScript-Beispiel
Erstelle die App mit npm create vite@latest d3-demo -- --template vanilla-ts, installiere npm i d3 @types/d3, ersetze die Dateien und starte npm run dev.
{
"scripts": {
"dev": "vite"
},
"dependencies": {
"d3": "^7.9.0"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"typescript": "latest",
"vite": "latest"
}
}
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>D3 Conversion Chart</title>
</head>
<body>
<main class="page">
<h1>D3.js Conversion Dashboard</h1>
<section class="chart-shell" aria-describedby="chart-summary">
<div id="chart"></div>
<p id="chart-summary" class="sr-only"></p>
</section>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
:root {
color: #172033;
background: #f7f7f3;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
}
.page {
width: min(920px, calc(100vw - 32px));
margin: 40px auto;
}
.chart-shell {
border: 1px solid #d8d5cc;
border-radius: 8px;
background: #ffffff;
padding: 20px;
}
#chart {
min-height: 320px;
position: relative;
}
#chart svg {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.axis-label {
fill: #475569;
font-size: 12px;
}
.bar {
fill: #2563eb;
outline: none;
}
.bar:hover,
.bar:focus {
fill: #dc2626;
}
.trend-line {
fill: none;
stroke: #0f172a;
stroke-width: 2;
pointer-events: none;
}
.chart-tooltip {
position: absolute;
top: 0;
left: 0;
max-width: 220px;
border-radius: 6px;
background: #172033;
color: #ffffff;
font-size: 13px;
line-height: 1.5;
opacity: 0;
padding: 8px 10px;
pointer-events: none;
transform: translate(-9999px, -9999px);
transition: opacity 120ms ease;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
import * as d3 from "d3";
import "./style.css";
type ChannelDatum = {
channel: string;
visitors: number;
conversions: number;
};
const data: ChannelDatum[] = [
{ channel: "Search", visitors: 4200, conversions: 168 },
{ channel: "Newsletter", visitors: 2600, conversions: 182 },
{ channel: "Social", visitors: 3100, conversions: 96 },
{ channel: "Partner", visitors: 1400, conversions: 84 },
];
const numberFormat = new Intl.NumberFormat(undefined);
const percentFormat = new Intl.NumberFormat(undefined, {
style: "percent",
maximumFractionDigits: 1,
});
function conversionRate(datum: ChannelDatum): number {
return datum.visitors === 0 ? 0 : datum.conversions / datum.visitors;
}
function drawConversionChart(container: HTMLElement, items: ChannelDatum[]): void {
container.replaceChildren();
if (items.length === 0) {
container.textContent = "No data to display.";
return;
}
const margin = { top: 28, right: 24, bottom: 56, left: 64 };
const outerWidth = 760;
const outerHeight = 420;
const width = outerWidth - margin.left - margin.right;
const height = outerHeight - margin.top - margin.bottom;
const svg = d3
.select(container)
.append("svg")
.attr("viewBox", `0 0 ${outerWidth} ${outerHeight}`)
.attr("role", "img")
.attr("aria-labelledby", "chart-title chart-desc");
svg.append("title").attr("id", "chart-title").text("Conversions by channel");
svg
.append("desc")
.attr("id", "chart-desc")
.text("Bar chart comparing conversions from each acquisition channel.");
const plot = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const x = d3
.scaleBand<string>()
.domain(items.map((d) => d.channel))
.range([0, width])
.padding(0.28);
const y = d3
.scaleLinear()
.domain([0, d3.max(items, (d) => d.conversions) ?? 0])
.nice()
.range([height, 0]);
plot
.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.call((axis) => axis.selectAll("text").attr("dy", "0.85em"));
plot.append("g").call(d3.axisLeft(y).ticks(5));
plot
.append("text")
.attr("class", "axis-label")
.attr("x", -margin.left + 4)
.attr("y", -10)
.text("Conversions");
const tooltip = d3.select(container).append("div").attr("class", "chart-tooltip");
function showTooltip(event: PointerEvent | FocusEvent, datum: ChannelDatum): void {
const xCenter = (x(datum.channel) ?? 0) + x.bandwidth() / 2 + margin.left;
const yTop = y(datum.conversions) + margin.top;
const [left, top] =
"clientX" in event ? d3.pointer(event, container) : [xCenter, yTop];
tooltip
.style("opacity", "1")
.style("transform", `translate(${left + 12}px, ${top - 28}px)`)
.html(
`<strong>${datum.channel}</strong><br />` +
`Visitors: ${numberFormat.format(datum.visitors)}<br />` +
`Conversions: ${numberFormat.format(datum.conversions)}<br />` +
`CVR: ${percentFormat.format(conversionRate(datum))}`,
);
}
function hideTooltip(): void {
tooltip.style("opacity", "0").style("transform", "translate(-9999px, -9999px)");
}
const bars = plot
.selectAll<SVGRectElement, ChannelDatum>("rect.bar")
.data(items, (d) => d.channel)
.join((enter) =>
enter
.append("rect")
.attr("class", "bar")
.attr("x", (d) => x(d.channel) ?? 0)
.attr("width", x.bandwidth())
.attr("y", height)
.attr("height", 0),
)
.attr("tabindex", 0)
.attr("role", "img")
.attr(
"aria-label",
(d) =>
`${d.channel}: ${numberFormat.format(d.conversions)} conversions, ${percentFormat.format(
conversionRate(d),
)} conversion rate`,
)
.on("pointerenter pointermove", showTooltip)
.on("focus", showTooltip)
.on("pointerleave blur", hideTooltip);
bars
.transition()
.duration(700)
.delay((_d, index) => index * 80)
.attr("x", (d) => x(d.channel) ?? 0)
.attr("width", x.bandwidth())
.attr("y", (d) => y(d.conversions))
.attr("height", (d) => height - y(d.conversions));
const trendLine = d3
.line<ChannelDatum>()
.x((d) => (x(d.channel) ?? 0) + x.bandwidth() / 2)
.y((d) => y(d.conversions))
.curve(d3.curveMonotoneX);
plot
.append("path")
.datum(items)
.attr("class", "trend-line")
.attr("d", trendLine);
}
const chart = document.querySelector<HTMLElement>("#chart");
if (!chart) {
throw new Error("Missing #chart element.");
}
drawConversionChart(chart, data);
const summary = document.querySelector<HTMLElement>("#chart-summary");
if (summary) {
const best = data.reduce((current, item) =>
conversionRate(item) > conversionRate(current) ? item : current,
);
summary.textContent = `Highest conversion rate: ${best.channel}, ${percentFormat.format(
conversionRate(best),
)}.`;
}
Kurzer Smoke-Test in der Browser-Konsole:
console.log(document.querySelectorAll("#chart rect.bar").length);
console.log(document.querySelector("#chart svg")?.getAttribute("role"));
console.log(document.querySelector("#chart-summary")?.textContent);
Drei plus Use Cases
| Use Case | Warum D3 passt | Aufgabe für Claude Code |
|---|---|---|
| Content-Funnel | Quelle, Kategorie und CTA-Klicks gemeinsam vergleichen | Eventplan aus Analytics übernehmen |
| SaaS-Dashboard | Nutzung, Retention und Plan-Unterschiede zeigen | Typen, Loading, Empty State und Segmente fixieren |
| Betriebsreports | Schwellenwerte und Ausreißer hervorheben | Rendering-Kosten mit Performance prüfen |
| A/B-Test-Auswertung | Conversion-Differenzen klarer als Tabellen zeigen | Berechnung und Darstellung trennen wie bei A/B testing |
Der Nutzen entsteht, wenn der Chart Umsatzhandlungen erklärt: Registrierung, Kauf, Kontakt, Verlängerung oder Aktivierung.
Typische Stolperfallen
| Fehler | Symptom | Lösung |
|---|---|---|
| Redraw ohne Cleanup | Achsen und Balken duplizieren sich | Container leeren oder Cleanup-Funktion zurückgeben |
| Skalen umgehen | Mobile Layouts oder neue Werte brechen | Werte immer durch scaleLinear, scaleBand oder passende Skalen schicken |
| Nur enter zeichnen | Alte Daten bleiben sichtbar | selection.data(...).join(...) nutzen |
| Tooltip nur per Maus | Tastaturnutzer verlieren Informationen | tabindex, aria-label und focus ergänzen |
| Framework-Konflikt | React oder Astro überschreibt D3-DOM | D3 auf einen eigenen Container begrenzen |
| Bedeutung nur über Farbe | Schwache Accessibility | Labels, Werte, Legende und Kontrast ergänzen; siehe Accessibility |
Bitte Claude Code um eine konkrete Review: doppelte Redraws, leere Arrays, Division durch null, Tastatur, Screenreader, mobile Breite und 1000 Datenpunkte.
SEO, Monetarisierung und Verifikation
D3.js-Artikel können Suchtraffic bringen, aber der Code muss mit einem messbaren Ziel verbunden sein. Verlinke auf TypeScript-Tipps, SEO und Designsysteme.
ClaudeCodeLab bietet Setup-Material, Review-Prompts, CLAUDE.md-Vorlagen und Beratung. Wenn dein Team Chart-Standards statt einzelner Snippets braucht, starte mit den Produkten oder Training und Beratung.
Beim praktischen Test war der größte Gewinn, Accessibility und Verifikation schon im ersten Prompt zu verlangen. aria-label, Tastaturfokus, Empty State und Smoke-Test nachträglich einzubauen ist deutlich teurer.
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.