Guía para implementar visualizaciones D3.js con Claude Code
Crea gráficos D3.js con Claude Code: TypeScript ejecutable, casos de uso, errores comunes, verificación y CTA.
Por qué combinar Claude Code y D3.js
El primer problema con D3.js no suele ser dibujar una barra. El problema aparece cuando hay que entender cómo se conectan scaleBand, axisLeft, selection.join y transition, y después mantener el gráfico con datos reales, redimensionado, accesibilidad y cambios de producto.
Claude Code ayuda porque puede leer el HTML, CSS, TypeScript, contratos de datos y convenciones del proyecto antes de generar código. D3.js une datos con DOM y SVG; si la petición es vaga, el resultado puede funcionar una vez, pero será difícil de revisar.
Usa esta guía junto con la documentación oficial: D3 Getting started, Joining data, d3-scale, d3-axis y d3-transition.
Modelo mental para principiantes
D3 convierte datos en píxeles y elementos SVG. Definir cada capa evita que el prompt sea ambiguo.
| Parte | Explicación simple | Papel en el ejemplo |
|---|---|---|
| selection | Elegir elementos del DOM | Crear un svg dentro de #chart |
| scale | Convertir valores en posiciones de pantalla | Pasar canales al eje x y conversiones al eje y |
| axis | Dibujar marcas y etiquetas | Mostrar canales y conteos de conversión |
| mark | Forma visible | Barras con rect y línea auxiliar con path |
| join | Emparejar filas de datos con nodos DOM | Actualizar barras cuando cambian los datos |
| transition | Animar un cambio visual | Hacer crecer las barras desde la base |
Un buen prompt no dice solo “haz un gráfico D3”. Incluye forma de datos, unidades, estados vacíos, teclado y prueba de verificación.
Crea un gráfico de barras responsive en Vite + TypeScript + D3 v7 para visitantes y conversiones por canal. Usa
selection.join, tooltip, foco de teclado,aria-label, estado sin datos y una prueba rápida en la consola del navegador.
Ejemplo D3.js + TypeScript ejecutable
Crea una app con npm create vite@latest d3-demo -- --template vanilla-ts, instala npm i d3 @types/d3, reemplaza estos archivos y ejecuta 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="es">
<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),
)}.`;
}
Prueba rápida en la consola:
console.log(document.querySelectorAll("#chart rect.bar").length);
console.log(document.querySelector("#chart svg")?.getAttribute("role"));
console.log(document.querySelector("#chart-summary")?.textContent);
Casos de uso reales
| Caso | Por qué D3.js encaja | Qué pedirle a Claude Code |
|---|---|---|
| Embudo de contenidos | Compara fuente, categoría y clics de CTA | Reutilizar la medición de analytics |
| Dashboard SaaS | Muestra uso de funciones, retención y diferencias por plan | Fijar tipos, loading, empty state y segmentos en TypeScript |
| Operaciones e incidencias | Resalta umbrales, anomalías y periodos | Revisar coste de render con performance |
| A/B testing | Enseña diferencias de conversión mejor que una tabla | Separar cálculo y visualización como en A/B testing |
El valor aparece cuando el gráfico explica acciones cercanas a ingresos: registro, compra, contacto, renovación o activación.
Errores comunes
| Error | Síntoma | Solución |
|---|---|---|
| Redibujar sin limpiar | Barras y ejes se duplican | Limpiar el contenedor o devolver una función de cleanup |
| Saltarse las escalas | El gráfico se rompe en móvil o con rangos nuevos | Pasar siempre por scaleLinear, scaleBand u otra escala |
| Usar solo enter | Quedan elementos antiguos | Usar selection.data(...).join(...) |
| Tooltip solo con ratón | Usuarios de teclado no ven la información | Añadir tabindex, aria-label y focus |
| Choque con frameworks | React o Astro sobrescribe el DOM | Encerrar D3 en un contenedor propio |
| Significado solo por color | Menor accesibilidad | Añadir etiquetas, valores y contraste; ver accesibilidad |
Pide una revisión concreta: duplicados al redibujar, arrays vacíos, división por cero, teclado, lectores de pantalla, ancho móvil y 1000 puntos de datos.
SEO, monetización y verificación
Un artículo de D3.js puede atraer búsquedas, pero conviene conectarlo con una acción de negocio y con enlaces internos como TypeScript tips, SEO y design system.
ClaudeCodeLab ofrece materiales de configuración, prompts de revisión, plantillas CLAUDE.md y consultoría. Si tu equipo necesita reglas de visualización y no solo un snippet, revisa la página de productos o training y consultoría.
En la prueba práctica, el mayor ahorro vino de pedir accesibilidad y verificación desde el primer prompt. Añadir aria-label, foco de teclado, estado vacío y smoke test al inicio fue más barato que corregirlo después.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Checklist de permisos antes de que Claude Code edite un sitio de cliente
Guía para agencias que quieren usar IA en landing pages sin tocar zonas sensibles.
Convierte tickets de soporte SaaS en pasos reproducibles con Claude Code
Flujo para transformar reportes vagos en pasos, evidencia y una nota útil para ingeniería.
Convierte tus notas viejas de Obsidian en instrucciones para Claude Code en 10 minutos
Rutina de 10 minutos para separar tus notas de Obsidian en hechos, decisiones y dudas, y darle a Claude Code instrucciones que sí funcionan.