Visualização de dados com Claude Code: dashboard de receita
Implemente visualização de dados com Claude Code: Recharts, D3, CSV, acessibilidade e checks com Playwright.
Comece pela decisão, não pelo gráfico
Pedir para o Claude Code “criar uma visualização de dados” costuma gerar uma demo bonita, mas nem sempre útil. Um dashboard de verdade começa pela decisão que precisa apoiar. No ClaudeCodeLab, pageviews não bastam. O que importa é saber se leitores terminam artigos, clicam em links internos, abrem páginas de produto, enviam formulários de consultoria ou avançam de uma checklist gratuita para um template pago.
Neste guia vamos criar um pequeno dashboard de análise de conteúdo e monetização com React, TypeScript, Recharts, um helper D3 pequeno, agregação de CSV, labels acessíveis, layout responsivo e screenshots com Playwright. Recharts é prático para gráficos em React. D3 funciona melhor como caixa de ferramentas para escalas, transformações e cálculos customizados. Para este caso, use Recharts na UI e D3 apenas onde o cálculo exigir.
Um contrato de dados é a promessa entre a exportação de analytics e a interface: date é data ISO, sessions é número, revenue é valor numérico em USD e channel vem de uma lista fechada. Agregação é transformar linhas brutas em resumos, como receita por canal ou taxa de conversão. Acessibilidade significa que o gráfico também comunica por títulos, tabela, contraste e controles, sem depender apenas de cor ou mouse.
Leia junto com analytics implementation, dashboard development e accessibility. Fontes oficiais: Claude Code docs, Recharts getting started, D3 getting started, Playwright screenshots e WCAG non-text contrast.
Casos de uso práticos
| Caso | Métricas | Gráfico | Ação |
|---|---|---|---|
| Melhorar artigos | Leitura completa, cliques em CTA, tráfego orgânico | Linha e barras | Ajustar introdução, links internos e CTA |
| Vender produtos | Cliques, receita, mix de canais | Barras e participação | Melhorar cards, preço e tabela comparativa |
| Treinamento e consultoria | Formulários, downloads, tráfego B2B | KPIs e funil | Deixar o caminho para consultoria mais claro |
| Qualidade AdSense | Leitura, scroll, saídas perto de anúncios | Tendência anotada | Preservar leitura antes de adicionar anúncios |
Escolha gráficos simples. Linha para tempo, barras para comparação, participação só com poucas categorias e cards KPI para números importantes. Evite dois eixos em dashboards iniciantes, pois receita e conversão podem parecer relacionadas só pela escala.
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.
Instalação
npm i recharts d3
npm i -D @types/d3 @playwright/test
Contrato de dados e agregação CSV
Defina os campos antes do gráfico. Isso reduz campos inventados, receita como texto e NaN aparecendo no tooltip.
// 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);
}
Para CSVs com aspas, vírgulas dentro de campos, decimais locais ou fusos misturados, use uma biblioteca de CSV e fixtures reais.
Dashboard em React
// 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)}>
{label}
</button>
))}
</div>
<figure aria-labelledby="dashboard-chart-title" style={{ margin: 0 }}>
<h3 id="dashboard-chart-title">Dashboard chart</h3>
<ResponsiveContainer width="100%" height={320}>
{view === "revenue" ? (
<BarChart data={summary}>
<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}>
<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>Bars start at zero, values are available in the table, and color is never the only label.</figcaption>
</figure>
<table aria-label="Channel summary">
<thead>
<tr>{["Channel", "Sessions", "Signups", "Revenue", "CVR", "Revenue/session"].map((heading) => <th key={heading} scope="col">{heading}</th>)}</tr>
</thead>
<tbody>
{summary.map((row) => (
<tr key={row.channel}>
<th scope="row">{row.channel}</th>
<td>{row.sessions.toLocaleString("en-US")}</td>
<td>{row.signups.toLocaleString("en-US")}</td>
<td>{money.format(row.revenue)}</td>
<td>{percent.format(row.conversionRate)}</td>
<td>{money.format(row.arps)}</td>
</tr>
))}
</tbody>
</table>
</section>
);
}
function MetricCard({ label, value }: { label: string; value: string }) {
return (
<div>
<p>{label}</p>
<strong>{value}</strong>
</div>
);
}
D3 apenas como helper
// 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]),
};
}
Falhas comuns
As principais falhas são eixo Y truncado em barras, CSV sujo, significado apenas por cor, ausência de loading/error/empty e falta de teste em mobile. Todas afetam decisões de receita: um CTA pode parecer vencedor, um anúncio pode parecer seguro ou um canal pode parecer melhor só por causa de um gráfico enganoso.
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
Screenshots não provam a matemática; a agregação precisa de testes unitários. Mas elas encontram gráficos vazios, labels sobrepostos, overflow horizontal e tabelas faltando.
Conectar com receita
Se o leitor termina um artigo e não visita a biblioteca de produtos, o CTA precisa melhorar. Se um artigo de consultoria tem tráfego e poucos leads, a página de treinamento e consultoria precisa ser mais clara. Para iniciantes, a checklist gratuita pode ser a próxima etapa natural.
Ao testar este fluxo, o melhor resultado veio de definir o contrato de dados antes do gráfico. Quando o prompt exige estados vazios, tabela, screenshots mobile e revisão contra gráficos enganosos, Claude Code entrega algo bem mais próximo de produção. CSV real ainda exige validação própria por causa de aspas, fusos e formatos de cada ferramenta.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.