Use Cases (Actualizado: 2/6/2026)

Visualización de datos con Claude Code: dashboard de ingresos

Implementa visualización de datos con Claude Code: Recharts, D3, CSV, accesibilidad y pruebas Playwright.

Visualización de datos con Claude Code: dashboard de ingresos

Empieza por la decisión, no por el gráfico

Pedir a Claude Code “crea una visualización de datos” suele generar una demo atractiva, pero no necesariamente una herramienta útil. Un dashboard serio empieza con la decisión que debe apoyar. En ClaudeCodeLab no basta con ver páginas vistas. Hay que saber si los lectores terminan los artículos, hacen clic en enlaces internos, visitan productos, piden consultoría o avanzan desde una checklist gratuita hacia una plantilla de pago.

En esta guía construiremos un dashboard pequeño de analítica de contenido y monetización con React, TypeScript, Recharts, un helper mínimo de D3, agregación de CSV, etiquetas accesibles, diseño responsive y capturas con Playwright. Recharts funciona bien como librería de componentes para React. D3 conviene entenderlo como una caja de herramientas de bajo nivel para escalas, transformaciones y cálculos especiales. Para este caso, usa Recharts para la interfaz y D3 solo donde realmente haga falta.

Un contrato de datos es el acuerdo entre la fuente de analítica y la UI: date es una fecha ISO, sessions es un número, revenue es un importe numérico en USD y channel pertenece a una lista cerrada. Agregación significa convertir filas crudas en resúmenes útiles, como ingresos por canal o tasa de conversión. Accesibilidad significa que el gráfico también se entiende con encabezados, tabla, contraste y controles operables sin depender solo del color o del ratón.

Para el contexto completo, revisa implementación de analítica, desarrollo de dashboards y accesibilidad. Las referencias oficiales son Claude Code docs, Recharts getting started, D3 getting started, Playwright screenshots y WCAG non-text contrast.

Casos de uso concretos

CasoMétricasGráfico recomendadoAcción
Mejora de artículosLectura completa, clics de CTA, tráfico orgánicoLínea y barrasReescribir introducción, enlaces internos y CTA
Venta de productosClics de producto, ingresos, mezcla de canalesBarras y participaciónMejorar tarjetas, precio y tabla comparativa
Formación y consultoríaFormularios completados, descargas, tráfico B2BKPI y embudoAclarar la ruta hacia la consultoría
Calidad AdSenseLectura, scroll, salidas cerca de anunciosTendencia anotadaProteger la experiencia antes de añadir anuncios

La elección del gráfico debe ser simple. Línea para evolución temporal, barras para comparar categorías, gráfico de participación solo con pocas categorías y tarjetas KPI para números críticos. Evita gráficos de doble eje en dashboards para principiantes: ingresos y conversión pueden parecer relacionados solo por cómo se escalaron los ejes.

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 para 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.

Este prompt obliga a Claude Code a entregar algo revisable: datos, estados, accesibilidad y evidencia visual.

Instalación

npm i recharts d3
npm i -D @types/d3 @playwright/test

Contrato de datos y agregación CSV

Define la forma de los datos antes de abrir el archivo del gráfico. Así evitas que el modelo invente campos, trate ingresos como texto o deje pasar NaN.

// 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);
}

Este parser es suficiente para exportaciones simples. Si tu CSV incluye campos entre comillas, comas dentro de columnas, decimales localizados o zonas horarias mezcladas, pide a Claude Code que use una librería de CSV y añada fixtures reales.

Componente de dashboard

El componente incluye KPI, pestañas de gráfico, estados de carga, error y vacío, contenedor responsive y una tabla de respaldo. La tabla permite revisar cifras exactas y mejora la experiencia para tecnologías de asistencia.

// 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 como ayuda puntual

// 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]),
  };
}

Errores que debes revisar

El primer error es truncar el eje Y de una gráfica de barras. Hace que diferencias pequeñas parezcan enormes. El segundo es enviar CSV sucio al gráfico: fechas inválidas, celdas vacías y canales desconocidos deben limpiarse antes. El tercero es depender solo del color. El cuarto es olvidar estados de carga, error y vacío. El quinto es no comprobar el ancho móvil, donde las etiquetas y tablas suelen romperse.

Pruebas visuales con Playwright

// 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

Las capturas no prueban la matemática; para eso necesitas tests unitarios de agregación. Sí detectan gráficos vacíos, solapamiento de etiquetas, overflow horizontal y tablas ausentes.

Conectar el dashboard con ingresos

La visualización debe llevar a decisiones. Si un lector termina un artículo pero no visita la biblioteca de productos, el CTA necesita trabajo. Si un artículo de consultoría recibe tráfico pero no genera formularios, la página de formación y consultoría debe prometer algo más claro. Si un principiante lee varias guías, la checklist gratuita puede ser el siguiente paso.

Al probar este patrón, lo más valioso fue fijar el contrato de datos antes de crear gráficos. Cuando el prompt exige estados vacíos, tabla, capturas móviles y revisión de gráficos engañosos, Claude Code produce una base mucho más publicable. El riesgo restante está en los CSV reales: comillas, zonas horarias y formatos de cada proveedor deben probarse con datos del proyecto.

#Claude Code #visualización de datos #Recharts #D3 #dashboard #React
Gratis

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.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.