تصور البيانات باستخدام الرسوم البيانية: أنماط لوحة المعلومات
لوحة المعلومات الرائعة ليست مجرد أرقام في جدول، بل هي أنماط مرئية تكشف عن الاتجاهات والحالات الشاذة والفرص في لمحة واحدة. Recharts هي مكتبة الرسوم البيانية الأكثر استخدامًا على نطاق واسع في نظام React البيئي: مبنية على أساسيات D3.js، مكتوبة بالكامل باستخدام TypeScript، وقابلة للتركيب، وسريعة الاستجابة. إنه يتعامل مع تعقيد SVG حتى تتمكن من التركيز على قصة البيانات.
يغطي هذا الدليل أنماط Recharts للوحات معلومات الإنتاج - اتجاهات الإيرادات، ومسارات التحويل، ومقاييس السلاسل الزمنية، والتوزيع الجغرافي، وبيانات التدفق في الوقت الفعلي - مع دعم الوضع المظلم، ولوحات الألوان التي يمكن الوصول إليها، والتخطيطات سريعة الاستجابة.
الوجبات الرئيسية
- قم دائمًا بلف المخططات في
ResponsiveContainerباستخدامwidth="100%"وheightثابت - لا تقم مطلقًا بترميز عرض البكسل بشكل ثابت- استخدم مكونات
CustomTooltipبدلاً من المكونات الافتراضية — تحكم كامل في التنسيق والتصميم- لا ينبغي أبدًا كتابة معلمات رد الاتصال الخاصة بإعادة التخطيط
formatterبشكل صريح - استخدمNumber(value)للتحويل- يتطلب الوضع الداكن قيم ألوان ديناميكية - يمكن قراءتها من متغيرات CSS، وليس الألوان السداسية المضمنة
- بالنسبة لبيانات السلاسل الزمنية، استخدم
tickFormatterعلى المحور X لتنسيق الطوابع الزمنية بشكل متسق- إمكانية الوصول: أضف
aria-labelإلى حاوية المخطط واستخدمtabIndexعلى عناصر وسيلة الإيضاح- الأداء: حفظ بيانات المخطط باستخدام
useMemo— إعادة تخطيط عمليات إعادة العرض في كل عرض أصلي- بالنسبة للبيانات في الوقت الفعلي، قم بتحديث مرجع مجموعة البيانات - يكتشف Recharts التغييرات حسب المرجع
الإعداد
pnpm add recharts
pnpm add -D @types/recharts # Usually not needed — recharts ships its own types
يقوم Recharts 2.x بشحن أنواع TypeScript. لا يلزم وجود حزمة @types منفصلة.
مخطط منطقة الإيرادات
// 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>
);
}
الرسم البياني الشريطي: المقارنة الشهرية
// 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>
);
}
دونات / مخطط دائري
// 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>
);
}
الرسم البياني الخطي في الوقت الحقيقي
// 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>
);
}
تخطيط لوحة المعلومات مع بطاقات الإحصائيات
// 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);
}
الأسئلة المتداولة
لماذا يجب ألا تقوم عمليات الاسترجاعات الخاصة بمنسق Recharts Tooltip أبدًا بكتابة المعلمات الخاصة بها بشكل صريح؟
يتم تحميل أنواع TypeScript الخاصة بـ Recharts للخاصية formatter بشكل زائد بطرق تسبب تعارضات في النوع عند إضافة تعليقات توضيحية إلى المعلمات. النمط الصحيح هو استخدام Number(value) لتحويل القيمة بأمان دون إضافة تعليق توضيحي لنوع value. إذا قمت بتعليقه كـ (value: number)، فقد يحدث خطأ في TypeScript بسبب نوع الاتحاد في التوقيع الداخلي لـ Recharts. اسمح لـ TypeScript باستنتاج النوع والتحويل باستخدام Number(value).
كيف أجعل مخططات Recharts تعمل في الوضع المظلم؟
استخدم خصائص CSS المخصصة (المتغيرات) لجميع الألوان بدلاً من القيم السداسية المضمنة. قم بالإشارة إليها باستخدام hsl(var(--border)) لخطوط الشبكة، وhsl(var(--muted-foreground)) لنص المحور، وhsl(var(--background)) لخلفيات تلميحات الأدوات. يتم تعريف هذه المتغيرات في سمة Tailwind الخاصة بك ويتم تبديل القيم تلقائيًا بين الوضع الفاتح والداكن عبر فئة dark على <html>. لا تستخدم الكود الثابت #374151 مطلقًا — فلن يتم تحديثه باستخدام السمة.
كيف أتعامل مع الحالات الفارغة أو حالات التحميل للمخططات؟
للتحميل: استبدل المخطط بالهيكل العظمي (animate-pulse bg-muted rounded-lg h-80). بالنسبة للبيانات الفارغة: قم بعرض رسالة حالة فارغة مركزية داخل ResponsiveContainer. بالنسبة للبيانات الجزئية: تتعامل ميزة Recharts مع البيانات المتفرقة بأمان - ما عليك سوى تمرير ما لديك وسيقوم بتوصيل النقاط (أو إظهار الفجوات للقيم الخالية مع connectNulls={false}).
هل يجب أن أستخدم Recharts أو Chart.js أو D3؟
إعادة التخطيط للوحات معلومات React الأصلية - فهي تعريفية وقابلة للتركيب وتتكامل بشكل طبيعي مع نموذج عرض React. Chart.js لحالات الاستخدام الأبسط حيث تريد عارضًا يستند إلى Canvas (أداء أفضل لأكثر من 10000 نقطة بيانات). D3 عندما تحتاج إلى أنواع مخططات مخصصة وغير قياسية لا تغطيها أي مكتبة - توقع المزيد من التعليمات البرمجية بشكل ملحوظ. تجنب خلط مكتبات الرسوم البيانية المتعددة في تطبيق واحد.
كيف يمكنني تصدير المخططات كصور للتقارير؟
استخدم مكتبة html2canvas لالتقاط حاوية المخطط كصورة: const canvas = await html2canvas(chartRef.current); const url = canvas.toDataURL('image/png');. بدلاً من ذلك، استخدم مخرجات SVG المضمنة في Recharts - نظرًا لأن Recharts يعرض SVG، يمكنك إجراء تسلسل لعنصر SVG مباشرة لتصدير المتجهات. بالنسبة لتقارير PDF، قم بعرض المخططات من جانب الخادم باستخدام محرك الدمى أو متصفح بدون رأس.
الخطوات التالية
يقوم تصور البيانات بتحويل المقاييس الأولية إلى ذكاء الأعمال. تمنحك الأنماط الموجودة في هذا الدليل — الحاويات سريعة الاستجابة، وتلميحات الأدوات المخصصة، والبث في الوقت الفعلي، والألوان المرتبطة بالموضوع — الأساس لواجهات مستخدم لوحة المعلومات التي تتواصل بوضوح على أي جهاز وبأي نظام ألوان.
تقوم ECOSIRE ببناء لوحات معلومات تحليلية باستخدام Recharts عبر 8 وحدات لوحة معلومات للمقاييس المالية والتشغيلية ومقاييس الأداء - مع البيانات المباشرة من واجهات برمجة التطبيقات NestJS وPostgreSQL التي تعمل بنظام Drizzle. بالنسبة لتطبيقات ذكاء الأعمال وPower BI، استكشف خدمات Power BI أو اعرض جميع خدماتنا.
بقلم
ECOSIRE Research and Development Team
بناء منتجات رقمية بمستوى المؤسسات في ECOSIRE. مشاركة رؤى حول تكاملات Odoo وأتمتة التجارة الإلكترونية وحلول الأعمال المدعومة بالذكاء الاصطناعي.
مقالات ذات صلة
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.