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
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.