Use Cases (Diperbarui: 2/6/2026)

Visualisasi Data dengan Claude Code: Dashboard Revenue

Bangun visualisasi data dengan Claude Code: Recharts, D3, agregasi CSV, aksesibilitas, dan screenshot Playwright.

Visualisasi Data dengan Claude Code: Dashboard Revenue

Mulai dari keputusan, bukan dari grafik

Meminta Claude Code “buatkan visualisasi data” biasanya menghasilkan demo yang terlihat rapi, tetapi belum tentu membantu keputusan bisnis. Dashboard yang berguna harus dimulai dari pertanyaan: siapa yang membaca angka ini dan keputusan apa yang akan dibuat? Untuk ClaudeCodeLab, pageview saja tidak cukup. Kita perlu tahu apakah pembaca menyelesaikan artikel, mengklik internal link, membuka halaman produk, mengirim form konsultasi, atau bergerak dari checklist gratis ke template berbayar.

Panduan ini membangun dashboard kecil untuk analytics konten dan monetisasi menggunakan React, TypeScript, Recharts, helper D3 kecil, agregasi CSV, label aksesibel, layout responsif, dan screenshot check dengan Playwright. Recharts cocok sebagai library komponen chart untuk React. D3 lebih tepat dipakai sebagai toolbox rendah level untuk scale, transformasi, dan kalkulasi khusus. Untuk dashboard praktis, gunakan Recharts dulu, lalu tambahkan D3 hanya saat diperlukan.

Data contract adalah kesepakatan antara sumber analytics dan UI: date berupa ISO date, sessions angka, revenue angka USD, dan channel berasal dari daftar tetap. Aggregation berarti merangkum baris mentah menjadi angka yang siap dipakai, seperti revenue per channel atau conversion rate. Aksesibilitas berarti chart tetap bisa dipahami lewat heading, tabel, kontras, dan kontrol keyboard, tidak hanya lewat warna atau mouse.

Baca juga analytics implementation, dashboard development, dan accessibility. Rujukan resmi: Claude Code docs, Recharts getting started, D3 getting started, Playwright screenshots, dan WCAG non-text contrast.

Use case nyata

Use caseMetrikChartAksi
Optimasi artikelRead completion, CTA click, organic trafficLine dan barPerbaiki intro, internal link, CTA
Penjualan produkProduct click, revenue, channel mixBar dan share chartPerbaiki product card, pricing, comparison table
Training dan konsultasiLead form, download, traffic B2BKPI dan funnelPerjelas jalur menuju konsultasi
Kualitas AdSenseRead depth, scroll, exit dekat iklanAnnotated trendJaga pengalaman baca sebelum menambah iklan

Pilihan chart sebaiknya sederhana. Gunakan line untuk tren waktu, bar untuk perbandingan kategori, share chart hanya untuk sedikit kategori, dan KPI card untuk angka utama. Hindari dual-axis chart untuk dashboard pemula karena revenue dan conversion rate bisa terlihat berhubungan hanya karena skala sumbu.

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

Prompt ini membuat Claude Code memikirkan data shape, state gagal, aksesibilitas, dan bukti visual sejak awal.

Instalasi

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

Data contract dan agregasi CSV

Kunci bentuk data sebelum membuat chart. Ini menghindari field yang ditebak, revenue yang dianggap string, dan NaN yang bocor ke 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);
}

Parser ini untuk CSV sederhana. Jika export memiliki quoted field, koma di dalam kolom, format desimal lokal, atau timezone campur, minta Claude Code memakai parser CSV khusus dan fixture dari data asli.

Komponen dashboard React

Komponen ini memiliki KPI card, tab chart, loading/error/empty state, responsive chart, dan table fallback. Tabel membantu audit angka dan membuat data tetap terbaca untuk assistive technology.

// 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 sebagai helper kecil

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

Pitfall yang harus dicek

Kesalahan pertama adalah bar chart dengan sumbu Y tidak mulai dari nol. Perbedaan kecil bisa terlihat besar dan memengaruhi keputusan CTA atau iklan. Kedua, CSV kotor: empty cell, date invalid, dan channel asing harus dibersihkan. Ketiga, makna hanya lewat warna. Keempat, tidak ada loading/error/empty state. Kelima, tidak mengecek mobile width seperti 390px.

Screenshot check dengan 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

Screenshot tidak membuktikan kalkulasi benar; agregasi tetap perlu unit test. Namun screenshot cepat menemukan chart kosong, label bertumpuk, overflow horizontal, dan tabel yang hilang.

Hubungkan dengan revenue path

Visualisasi data bukan dekorasi. Jika pembaca selesai membaca tetapi tidak membuka produk, CTA perlu diperbaiki. Jika artikel konsultasi punya traffic tapi tidak menghasilkan lead, halaman training dan konsultasi perlu dibuat lebih jelas. Untuk pemula, checklist gratis bisa menjadi langkah berikutnya.

Saat pola ini dicoba, hasil terbaik muncul ketika data contract ditentukan sebelum chart dibuat. Jika prompt sejak awal meminta empty state, error state, table fallback, screenshot mobile, dan review chart yang menyesatkan, Claude Code menghasilkan dashboard yang jauh lebih siap produksi. Risiko tersisa ada pada CSV nyata: quotes, timezone, dan format tiap analytics tool tetap harus diuji dengan sample asli.

#Claude Code #visualisasi data #Recharts #D3 #dashboard #React
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.