Data Visualization with Recharts: Dashboard Patterns

Build production-ready dashboards with Recharts in React. Covers bar, line, area, pie charts, responsive containers, custom tooltips, real-time data, and dark mode support.

E
ECOSIRE Research and Development Team
|19 Mart 20269 dk okuma1.9k Kelime|

Yeniden Grafiklerle Veri Görselleştirme: Kontrol Paneli Modelleri

Harika bir gösterge tablosu yalnızca tablodaki rakamlardan ibaret değildir; trendleri, anormallikleri ve fırsatları bir bakışta ortaya çıkaran görsel kalıplardır. Recharts, React ekosisteminde en yaygın kullanılan grafik kitaplığıdır: D3.js temelleri üzerine inşa edilmiştir, tamamen TypeScript ile yazılmıştır, oluşturulabilir ve kutudan çıktığı gibi duyarlıdır. Veri hikayesine odaklanabilmeniz için SVG karmaşıklığını yönetir.

Bu kılavuz, karanlık mod desteği, erişilebilir renk paletleri ve duyarlı düzenler ile üretim kontrol panellerine (gelir eğilimleri, dönüşüm hunileri, zaman serisi ölçümleri, coğrafi dağıtım ve gerçek zamanlı akış verileri) yönelik Recharts modellerini kapsar.

Önemli Çıkarımlar

  • Grafikleri her zaman ResponsiveContainer ile width="100%" ve sabit bir height ile sarın; piksel genişliklerini asla sabit kodlamayın
  • Varsayılan yerine CustomTooltip bileşenlerini kullanın — biçimlendirme ve stil üzerinde tam kontrol
  • Yeniden çizelgeler formatter geri çağırma parametreleri ASLA açıkça yazılmamalıdır — dönüştürmek için Number(value) kullanın
  • Koyu mod, dinamik renk değerleri gerektirir; sabit kodlanmış onaltılık renklerden değil, CSS değişkenlerinden okunur
  • Zaman serisi verileri için, zaman damgalarını tutarlı bir şekilde biçimlendirmek üzere XAxis üzerinde bir tickFormatter kullanın
  • Erişilebilirlik: grafik kapsayıcısına aria-label ekleyin ve gösterge öğelerinde tabIndex kullanın
  • Performans: grafik verilerini useMemo ile not edin — Her ana oluşturmada yeniden grafikler yeniden oluşturulur
  • Gerçek zamanlı veriler için veri dizisi referansını güncelleyin — Yeniden çizelgeler, değişiklikleri referansa göre algılar

Kurulum

pnpm add recharts
pnpm add -D @types/recharts  # Usually not needed — recharts ships its own types

Recharts 2.x, TypeScript türlerini sunar. Ayrı bir @types paketi gerekmez.


Gelir Alanı Tablosu

// src/components/charts/revenue-area-chart.tsx
'use client';

import { useMemo } from 'react';
import {
  AreaChart, Area, XAxis, YAxis, CartesianGrid,
  Tooltip, Legend, ResponsiveContainer,
} from 'recharts';
import { format, parseISO } from 'date-fns';

interface RevenueDataPoint {
  date: string;       // ISO date string
  revenue: number;
  refunds: number;
  net: number;
}

interface CustomTooltipProps {
  active?: boolean;
  payload?: Array<{ name: string; value: number; color: string }>;
  label?: string;
}

function CustomTooltip({ active, payload, label }: CustomTooltipProps) {
  if (!active || !payload || !label) return null;

  return (
    <div className="rounded-lg border bg-background p-3 shadow-md">
      <p className="mb-2 text-sm font-medium text-muted-foreground">
        {format(parseISO(label), 'MMM d, yyyy')}
      </p>
      {payload.map((entry) => (
        <div key={entry.name} className="flex items-center gap-2 text-sm">
          <span
            className="inline-block h-2 w-2 rounded-full"
            style={{ backgroundColor: entry.color }}
          />
          <span className="text-muted-foreground">{entry.name}:</span>
          <span className="font-medium">
            {new Intl.NumberFormat('en-US', {
              style: 'currency',
              currency: 'USD',
              maximumFractionDigits: 0,
            }).format(entry.value)}
          </span>
        </div>
      ))}
    </div>
  );
}

interface RevenueAreaChartProps {
  data: RevenueDataPoint[];
  loading?: boolean;
}

export function RevenueAreaChart({ data, loading }: RevenueAreaChartProps) {
  // CRITICAL: memoize chart data — Recharts re-renders on every reference change
  const chartData = useMemo(() => data, [data]);

  if (loading) {
    return <div className="h-80 animate-pulse rounded-lg bg-muted" />;
  }

  return (
    <div role="img" aria-label="Monthly revenue area chart">
      <ResponsiveContainer width="100%" height={320}>
        <AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
          <defs>
            <linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
              <stop offset="5%"  stopColor="#f59e0b" stopOpacity={0.3} />
              <stop offset="95%" stopColor="#f59e0b" stopOpacity={0} />
            </linearGradient>
            <linearGradient id="colorNet" x1="0" y1="0" x2="0" y2="1">
              <stop offset="5%"  stopColor="#22c55e" stopOpacity={0.3} />
              <stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
            </linearGradient>
          </defs>

          <CartesianGrid
            strokeDasharray="3 3"
            stroke="hsl(var(--border))"
            vertical={false}
          />

          <XAxis
            dataKey="date"
            tickFormatter={(value: string) => format(parseISO(value), 'MMM d')}
            tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
            axisLine={false}
            tickLine={false}
          />

          <YAxis
            tickFormatter={(value) => `$${(Number(value) / 1000).toFixed(0)}k`}
            tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
            axisLine={false}
            tickLine={false}
            width={60}
          />

          <Tooltip content={<CustomTooltip />} />

          <Legend
            wrapperStyle={{ fontSize: '14px', paddingTop: '16px' }}
            formatter={(value: string) =>
              <span className="text-foreground">{value}</span>
            }
          />

          <Area
            type="monotone"
            dataKey="revenue"
            name="Gross Revenue"
            stroke="#f59e0b"
            strokeWidth={2}
            fill="url(#colorRevenue)"
          />
          <Area
            type="monotone"
            dataKey="net"
            name="Net Revenue"
            stroke="#22c55e"
            strokeWidth={2}
            fill="url(#colorNet)"
          />
        </AreaChart>
      </ResponsiveContainer>
    </div>
  );
}

Çubuk Grafik: Aylık Karşılaştırma

// src/components/charts/monthly-bar-chart.tsx
'use client';

import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid,
  Tooltip, Legend, ResponsiveContainer, Cell,
} from 'recharts';

interface MonthlyData {
  month: string;
  current: number;
  previous: number;
  target: number;
}

function CustomBarTooltip({ active, payload, label }: any) {
  if (!active || !payload) return null;

  return (
    <div className="rounded-lg border bg-background p-3 shadow-md">
      <p className="mb-2 font-medium">{label}</p>
      {payload.map((entry: { name: string; value: number; fill: string }) => (
        <div key={entry.name} className="flex items-center gap-2 text-sm">
          <span
            className="inline-block h-2 w-2 rounded-full"
            style={{ backgroundColor: entry.fill }}
          />
          <span className="text-muted-foreground">{entry.name}:</span>
          <span className="font-medium">
            {new Intl.NumberFormat('en-US', {
              style: 'currency',
              currency: 'USD',
              maximumFractionDigits: 0,
            }).format(entry.value)}
          </span>
        </div>
      ))}
    </div>
  );
}

export function MonthlyBarChart({ data }: { data: MonthlyData[] }) {
  return (
    <ResponsiveContainer width="100%" height={300}>
      <BarChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" vertical={false} />
        <XAxis
          dataKey="month"
          tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
          axisLine={false}
          tickLine={false}
        />
        <YAxis
          tickFormatter={(value) => `$${(Number(value) / 1000).toFixed(0)}k`}
          tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
          axisLine={false}
          tickLine={false}
        />
        <Tooltip content={<CustomBarTooltip />} />
        <Legend />
        <Bar dataKey="previous" name="Previous Year" fill="hsl(var(--muted))" radius={[4, 4, 0, 0]} />
        <Bar dataKey="current"  name="Current Year"  fill="#f59e0b"            radius={[4, 4, 0, 0]} />
        <Bar dataKey="target"   name="Target"        fill="#22c55e" opacity={0.4} radius={[4, 4, 0, 0]} />
      </BarChart>
    </ResponsiveContainer>
  );
}

Halka / Pasta Grafiği

// src/components/charts/platform-pie-chart.tsx
'use client';

import { useState } from 'react';
import {
  PieChart, Pie, Cell, Sector,
  ResponsiveContainer, Legend, Tooltip,
} from 'recharts';

interface PieDataPoint {
  name: string;
  value: number;
  color: string;
}

const PLATFORM_COLORS: Record<string, string> = {
  'Odoo':         '#714b67',
  'Shopify':      '#96bf48',
  'GoHighLevel':  '#f59e0b',
  'Power BI':     '#f2c811',
  'OpenClaw':     '#0ea5e9',
  'Other':        '#94a3b8',
};

function renderActiveShape(props: any) {
  const {
    cx, cy, innerRadius, outerRadius, startAngle, endAngle,
    fill, payload, percent, value,
  } = props;

  return (
    <g>
      <text x={cx} y={cy - 12} textAnchor="middle" className="text-base font-bold fill-foreground">
        {payload.name}
      </text>
      <text x={cx} y={cy + 12} textAnchor="middle" className="text-sm fill-muted-foreground">
        {`${(percent * 100).toFixed(1)}%`}
      </text>
      <text x={cx} y={cy + 32} textAnchor="middle" className="text-sm fill-muted-foreground">
        {`$${(Number(value) / 1000).toFixed(0)}k`}
      </text>
      <Sector
        cx={cx} cy={cy}
        innerRadius={innerRadius}
        outerRadius={outerRadius + 8}
        startAngle={startAngle}
        endAngle={endAngle}
        fill={fill}
      />
      <Sector
        cx={cx} cy={cy}
        innerRadius={outerRadius + 10}
        outerRadius={outerRadius + 12}
        startAngle={startAngle}
        endAngle={endAngle}
        fill={fill}
      />
    </g>
  );
}

export function PlatformPieChart({ data }: { data: PieDataPoint[] }) {
  const [activeIndex, setActiveIndex] = useState(0);

  const chartData = data.map((item) => ({
    ...item,
    color: PLATFORM_COLORS[item.name] ?? '#94a3b8',
  }));

  return (
    <ResponsiveContainer width="100%" height={300}>
      <PieChart>
        <Pie
          activeIndex={activeIndex}
          activeShape={renderActiveShape}
          data={chartData}
          cx="50%"
          cy="50%"
          innerRadius={70}
          outerRadius={100}
          dataKey="value"
          onMouseEnter={(_, index) => setActiveIndex(index)}
        >
          {chartData.map((entry, index) => (
            <Cell key={`cell-${index}`} fill={entry.color} />
          ))}
        </Pie>
        <Legend
          formatter={(value: string) =>
            <span className="text-sm text-foreground">{value}</span>
          }
        />
      </PieChart>
    </ResponsiveContainer>
  );
}

Gerçek Zamanlı Çizgi Grafiği

// src/components/charts/realtime-chart.tsx
'use client';

import { useState, useEffect, useRef } from 'react';
import {
  LineChart, Line, XAxis, YAxis, CartesianGrid,
  Tooltip, ResponsiveContainer, ReferenceLine,
} from 'recharts';

interface MetricPoint {
  time: string;
  requests: number;
  errors: number;
  p95: number;
}

const MAX_DATA_POINTS = 60; // 60 seconds of data

export function RealtimeChart({ endpoint }: { endpoint: string }) {
  const [data, setData] = useState<MetricPoint[]>([]);
  const intervalRef = useRef<ReturnType<typeof setInterval>>();

  useEffect(() => {
    const fetchMetrics = async () => {
      try {
        const response = await fetch(endpoint);
        const point: MetricPoint = await response.json();

        setData((prev) => {
          const updated = [
            ...prev,
            { ...point, time: new Date().toLocaleTimeString() },
          ];
          // Keep only the last MAX_DATA_POINTS — sliding window
          return updated.slice(-MAX_DATA_POINTS);
        });
      } catch {
        // Metrics fetch failure is non-critical — log but do not crash
      }
    };

    fetchMetrics();
    intervalRef.current = setInterval(fetchMetrics, 1000);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [endpoint]);

  return (
    <ResponsiveContainer width="100%" height={200}>
      <LineChart data={data}>
        <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
        <XAxis
          dataKey="time"
          tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
          interval="preserveStartEnd"
        />
        <YAxis
          tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
          axisLine={false}
        />
        <Tooltip />
        {/* Reference line for SLO threshold */}
        <ReferenceLine
          y={200}
          label={{ value: 'SLO 200ms', position: 'right', fontSize: 10 }}
          stroke="#ef4444"
          strokeDasharray="4 4"
        />
        <Line
          type="monotone"
          dataKey="p95"
          name="P95 Latency (ms)"
          stroke="#f59e0b"
          strokeWidth={2}
          dot={false}
          isAnimationActive={false} // Disable animation for real-time data
        />
        <Line
          type="monotone"
          dataKey="errors"
          name="Errors/s"
          stroke="#ef4444"
          strokeWidth={2}
          dot={false}
          isAnimationActive={false}
        />
      </LineChart>
    </ResponsiveContainer>
  );
}

İstatistik Kartlarıyla Kontrol Paneli Düzeni

// src/components/dashboard/metrics-dashboard.tsx
'use client';

import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api';
import { RevenueAreaChart } from './revenue-area-chart';
import { MonthlyBarChart } from './monthly-bar-chart';
import { PlatformPieChart } from './platform-pie-chart';

export function MetricsDashboard() {
  const { data: metrics, isLoading } = useQuery({
    queryKey: ['dashboard-metrics'],
    queryFn: () => apiFetch<DashboardMetrics>('/analytics/dashboard'),
    refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes
  });

  return (
    <div className="space-y-6">
      {/* KPI Cards */}
      <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
        {[
          { label: 'MRR', value: metrics?.mrr, format: 'currency', trend: '+12%' },
          { label: 'ARR',  value: metrics?.arr,  format: 'currency', trend: '+8%'  },
          { label: 'Customers', value: metrics?.customers, format: 'number', trend: '+23' },
          { label: 'Churn', value: metrics?.churnRate, format: 'percent', trend: '-0.3%' },
        ].map(({ label, value, format, trend }) => (
          <div key={label} className="rounded-lg border bg-card p-4">
            <p className="text-sm text-muted-foreground">{label}</p>
            <p className="mt-1 text-2xl font-bold">
              {isLoading ? (
                <span className="inline-block h-8 w-24 animate-pulse rounded bg-muted" />
              ) : (
                formatMetric(value, format)
              )}
            </p>
            <p className="mt-1 text-sm text-green-500">{trend} this month</p>
          </div>
        ))}
      </div>

      {/* Charts */}
      <div className="grid gap-6 lg:grid-cols-3">
        <div className="lg:col-span-2 rounded-lg border bg-card p-6">
          <h3 className="mb-4 text-lg font-semibold">Revenue Trend</h3>
          <RevenueAreaChart data={metrics?.revenueByDay ?? []} loading={isLoading} />
        </div>
        <div className="rounded-lg border bg-card p-6">
          <h3 className="mb-4 text-lg font-semibold">Revenue by Platform</h3>
          <PlatformPieChart data={metrics?.revenueByPlatform ?? []} />
        </div>
      </div>

      <div className="rounded-lg border bg-card p-6">
        <h3 className="mb-4 text-lg font-semibold">Monthly Comparison</h3>
        <MonthlyBarChart data={metrics?.monthlyComparison ?? []} />
      </div>
    </div>
  );
}

function formatMetric(value: number | undefined, format: string): string {
  if (value === undefined) return '—';
  const fmt = new Intl.NumberFormat('en-US');
  if (format === 'currency') return `$${fmt.format(value)}`;
  if (format === 'percent') return `${value.toFixed(1)}%`;
  return fmt.format(value);
}

Sıkça Sorulan Sorular

Neden Recharts Araç İpucu biçimlendirici geri çağrılarının parametrelerini açıkça yazmaları gerekmiyor?

formatter prop için Recharts'ın TypeScript türleri, parametrelere açıklama eklediğinizde tür çakışmalarına neden olacak şekilde aşırı yüklenmiştir. Doğru kalıp, value türüne açıklama eklemeden değeri güvenli bir şekilde dönüştürmek için Number(value) kullanmaktır. Bunu (value: number) olarak eklerseniz TypeScript, Recharts'ın dahili imzasındaki birleşim türü nedeniyle hata verebilir. TypeScript'in türü çıkarmasına ve Number(value) ile dönüştürmesine izin verin.

Recharts grafiklerinin karanlık modda çalışmasını nasıl sağlayabilirim?

Tüm renkler için sabit kodlanmış onaltılık değerler yerine CSS özel özelliklerini (değişkenlerini) kullanın. Kılavuz çizgileri için hsl(var(--border)), eksen metni için hsl(var(--muted-foreground)) ve araç ipucu arka planları için hsl(var(--background)) ile bunlara referans verin. Bu değişkenler Tailwind temanızda tanımlanır ve <html> üzerindeki dark sınıfı aracılığıyla değerleri açık ve karanlık mod arasında otomatik olarak değiştirir. Hiçbir zaman #374151 kodunu sabit olarak yazmayın; temayla birlikte güncellenmez.

Grafiklerin boş veya yükleme durumlarını nasıl halledebilirim?

Yükleme için: Grafiği bir iskeletle değiştirin (animate-pulse bg-muted rounded-lg h-80). Boş veriler için: ResponsiveContainer içinde ortalanmış bir boş durum mesajı oluşturun. Kısmi veriler için: Recharts seyrek verileri zarif bir şekilde işler; yalnızca elinizde olanı iletin ve noktaları birleştirir (veya connectNulls={false} ile boş değerler için boşlukları gösterir).

Recharts'ı mı yoksa Chart.js'yi mi yoksa D3'ü mü kullanmalıyım?

React-yerel kontrol panelleri için yeniden çizelgeler; bildirimseldir, düzenlenebilir ve React'in oluşturma modeliyle doğal olarak bütünleşir. Canvas tabanlı bir oluşturucu istediğiniz daha basit kullanım durumları için Chart.js (10.000'den fazla veri noktası için daha iyi performans). Hiçbir kitaplığın kapsamadığı özel, standart dışı grafik türlerine ihtiyacınız olduğunda D3; önemli ölçüde daha fazla kod bekleyebilirsiniz. Birden fazla grafik kitaplığını tek bir uygulamada karıştırmaktan kaçının.

Grafikleri raporlar için görseller olarak nasıl dışa aktarırım?

Grafik kapsayıcısını resim olarak yakalamak için html2canvas kitaplığını kullanın: const canvas = await html2canvas(chartRef.current); const url = canvas.toDataURL('image/png');. Alternatif olarak, Recharts'ın yerleşik SVG çıkışını kullanın; Recharts, SVG'yi oluşturduğundan, vektör dışa aktarımı için SVG öğesini doğrudan seri hale getirebilirsiniz. PDF raporları için Puppeteer veya başsız bir tarayıcı kullanarak grafikleri sunucu tarafında işleyin.


Sonraki Adımlar

Veri görselleştirme, ham metrikleri iş zekasına dönüştürür. Bu kılavuzdaki modeller (duyarlı kapsayıcılar, özel araç ipuçları, gerçek zamanlı akış ve temaya duyarlı renkler) size herhangi bir cihazda ve herhangi bir renk şemasında net bir şekilde iletişim kuran kontrol paneli kullanıcı arayüzleri için temel sağlar.

ECOSIRE, NestJS API'lerinden ve Drizzle destekli PostgreSQL'den gelen canlı verilerle finansal, operasyonel ve performans ölçümleri için 8 gösterge paneli modülünde Rechart'larla analitik gösterge tabloları oluşturur. İş zekası ve Power BI uygulamaları için Power BI hizmetlerimizi keşfedin veya tüm hizmetlerimizi görüntüleyin.

E

Yazan

ECOSIRE Research and Development Team

ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.

WhatsApp'ta Sohbet Et