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
ResponsiveContainerilewidth="100%"ve sabit birheightile sarın; piksel genişliklerini asla sabit kodlamayın- Varsayılan yerine
CustomTooltipbileşenlerini kullanın — biçimlendirme ve stil üzerinde tam kontrol- Yeniden çizelgeler
formattergeri çağırma parametreleri ASLA açıkça yazılmamalıdır — dönüştürmek içinNumber(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
tickFormatterkullanın- Erişilebilirlik: grafik kapsayıcısına
aria-labelekleyin ve gösterge öğelerindetabIndexkullanın- Performans: grafik verilerini
useMemoile 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.
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.
İlgili Makaleler
Building Financial Dashboards with Power BI
Step-by-step guide to building financial dashboards in Power BI covering data connections to accounting systems, DAX measures for KPIs, P&L visualisations, and best practices.
GoHighLevel Reporting and Analytics: Measuring What Matters
Master GoHighLevel reporting and analytics. Learn to build custom dashboards, track ROI across channels, measure funnel conversion, and make data-driven marketing decisions.
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.