Claude Code D3.js Data Visualization Implementation Guide
Build D3.js charts with Claude Code: runnable TypeScript, use cases, pitfalls, verification, and revenue CTA.
Why Claude Code Helps With D3.js
The hard part of D3.js is rarely drawing the first rectangle. The hard part is understanding how scaleBand, axisLeft, selection.join, and transition fit together before a chart has to survive real data, resizing, accessibility review, and product changes.
Claude Code is useful because it can read the surrounding HTML, CSS, TypeScript, data contracts, and project conventions before generating code. D3.js is a library for binding data to the DOM and SVG, so vague instructions often produce a chart that works once but becomes painful to maintain.
This guide uses a copy-paste Vite + TypeScript example and shows what to ask Claude Code for. Keep the official docs open while adapting it: D3 Getting started, Joining data, d3-scale, d3-axis, and d3-transition.
The Mental Model for Beginners
D3 turns data into pixels and SVG elements. If you name each layer clearly, your prompts and reviews become much sharper.
| Part | Plain-English meaning | Role in this example |
|---|---|---|
| selection | Pick DOM elements | Add an svg inside #chart |
| scale | Convert data values to screen positions | Map channels to x positions and conversions to y positions |
| axis | Draw ticks and labels | Show channels and conversion counts |
| mark | Visible shape | Draw bars with rect and a guide line with path |
| join | Match data rows to DOM elements | Update bars when the dataset changes |
| transition | Animate a visual change | Grow bars from the baseline |
Do not ask Claude Code only for “a D3 chart.” Give it the data shape, units, empty states, keyboard behavior, and verification steps. A useful prompt is:
Build a responsive bar chart in Vite + TypeScript + D3 v7 for acquisition-channel visitors and conversions. Use
selection.join, tooltips, keyboard focus,aria-label, empty-data handling, and a browser-console smoke test.
Runnable D3.js + TypeScript Example
Create a vanilla TypeScript Vite app with npm create vite@latest d3-demo -- --template vanilla-ts, then install D3 with npm i d3 @types/d3. Replace the files below and run npm run dev. The D3 dependency targets the v7 line shown in the official docs.
{
"scripts": {
"dev": "vite"
},
"dependencies": {
"d3": "^7.9.0"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"typescript": "latest",
"vite": "latest"
}
}
<!doctype html>
<html lang="en">
<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),
)}.`;
}
After the page renders, run this in the browser console as a quick smoke test.
console.log(document.querySelectorAll("#chart rect.bar").length);
console.log(document.querySelector("#chart svg")?.getAttribute("role"));
console.log(document.querySelector("#chart-summary")?.textContent);
Practical Use Cases
| Use case | Why D3 fits | What Claude Code should handle |
|---|---|---|
| Content funnel analysis | Compare traffic source, article category, and CTA clicks on one screen | Reuse the event plan from analytics implementation |
| SaaS product dashboard | Show feature usage, retention, and plan-level differences | Lock data types, loading states, empty states, and segment labels in TypeScript |
| Operations and incident reports | Highlight time ranges, thresholds, and outliers | Review rendering cost with performance optimization |
| A/B test reporting | Show segment differences and conversion deltas more clearly than a table | Keep aggregation separate from the visualization, as in A/B testing |
The business value comes when the chart explains behavior that affects revenue: signup, purchase, contact, renewal, or product activation. D3 is strongest when a generic chart library cannot express that shape cleanly.
Pitfalls to Review Critically
| Pitfall | Symptom | Fix |
|---|---|---|
| Redrawing without cleanup | Axes and bars duplicate after navigation or state changes | Clear the container or return a cleanup function |
| Bypassing scales | The chart breaks on mobile or new data ranges | Route every data value through scaleLinear, scaleBand, or another D3 scale |
| Drawing only enter selections | Removed data leaves stale DOM behind | Use selection.data(...).join(...) as the default update pattern |
| Mouse-only tooltips | Keyboard and screen-reader users miss the same information | Add tabindex, aria-label, and focus behavior |
| Framework conflicts | React or Astro overwrites DOM that D3 changed | Confine D3 to one owned container |
| Color-only meaning | The chart fails for color-vision differences or printouts | Add labels, values, legends, and accessible contrast; see accessibility |
Ask Claude Code for a focused review: “Check duplicate redraws, empty arrays, divide-by-zero, keyboard access, screen-reader output, mobile width, and performance with 1,000 data points.” That produces much better feedback than “find bugs.”
SEO and Revenue Path
D3.js content can attract search traffic, but readers often copy the code and leave. Connect the chart to a measurable business outcome and link onward to TypeScript tips, SEO optimization, and design systems.
ClaudeCodeLab offers Claude Code setup material, review prompts, CLAUDE.md templates, and implementation consulting. For teams that need chart standards rather than a one-off snippet, start with the products page or the training and consultation page.
Hands-On Verification Note
When I tested this structure, the biggest quality improvement came from asking for accessibility and verification before the first implementation pass. Adding aria-label, keyboard focus, empty-data behavior, and a console smoke test upfront was cheaper than retrofitting them later.
The practical lesson is simple: use Claude Code and D3.js to capture the data meaning, display intent, verification checklist, and revenue path in one specification. That is what turns a chart from a visual decoration into a useful product surface.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.