रिचार्ज के साथ डेटा विज़ुअलाइज़ेशन: डैशबोर्ड पैटर्न
एक महान डैशबोर्ड केवल तालिका में संख्याएँ नहीं है - यह दृश्य पैटर्न है जो एक नज़र में रुझान, विसंगतियों और अवसरों को प्रकट करता है। रिएक्ट इकोसिस्टम में रीचार्ट्स सबसे व्यापक रूप से उपयोग की जाने वाली चार्टिंग लाइब्रेरी है: D3.js बुनियादी सिद्धांतों पर निर्मित, पूरी तरह से टाइपस्क्रिप्ट-टाइप, कंपोज़ेबल और बॉक्स से बाहर प्रतिक्रियाशील। यह एसवीजी जटिलता को संभालता है ताकि आप डेटा स्टोरी पर ध्यान केंद्रित कर सकें।
यह मार्गदर्शिका उत्पादन डैशबोर्ड के लिए रिचार्ज पैटर्न को कवर करती है - राजस्व रुझान, रूपांतरण फ़नल, समय-श्रृंखला मेट्रिक्स, भौगोलिक वितरण और वास्तविक समय स्ट्रीमिंग डेटा - डार्क मोड समर्थन, सुलभ रंग पैलेट और उत्तरदायी लेआउट के साथ।
मुख्य बातें
- चार्ट को हमेशा
ResponsiveContainerमेंwidth="100%"और एक निश्चितheightके साथ लपेटें - कभी भी पिक्सेल चौड़ाई को हार्डकोड न करें- डिफ़ॉल्ट के बजाय
CustomTooltipघटकों का उपयोग करें - फ़ॉर्मेटिंग और स्टाइल पर पूर्ण नियंत्रण- रीचार्ट्स
formatterकॉलबैक पैरामीटर को कभी भी स्पष्ट रूप से टाइप नहीं किया जाना चाहिए - कनवर्ट करने के लिएNumber(value)का उपयोग करें- डार्क मोड के लिए गतिशील रंग मानों की आवश्यकता होती है - सीएसएस वेरिएबल्स से पढ़ें, हार्डकोडेड हेक्स रंगों से नहीं
- समय-श्रृंखला डेटा के लिए, टाइमस्टैम्प को लगातार प्रारूपित करने के लिए XAxis पर
tickFormatterका उपयोग करें- अभिगम्यता: चार्ट कंटेनर में
aria-labelजोड़ें और लेजेंड आइटम परtabIndexका उपयोग करें- प्रदर्शन:
useMemoके साथ चार्ट डेटा को याद रखें - प्रत्येक मूल रेंडर पर री-रेंडर को रीचार्ट करता है- वास्तविक समय डेटा के लिए, डेटा सरणी संदर्भ को अपडेट करें - रिचार्ज संदर्भ द्वारा परिवर्तनों का पता लगाता है
सेटअप
pnpm add recharts
pnpm add -D @types/recharts # Usually not needed — recharts ships its own types
रीचार्ट्स 2.x टाइपस्क्रिप्ट प्रकारों को शिप करता है। कोई अलग @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);
}
अक्सर पूछे जाने वाले प्रश्न
रिचार्ट्स टूलटिप फ़ॉर्मेटर कॉलबैक को कभी भी अपने पैरामीटर स्पष्ट रूप से क्यों टाइप नहीं करने चाहिए?
formatter प्रोप के लिए रीचार्ट्स के टाइपस्क्रिप्ट प्रकार इस तरह से अतिभारित होते हैं कि जब आप मापदंडों को एनोटेट करते हैं तो प्रकार के टकराव का कारण बनते हैं। value के प्रकार को एनोटेट किए बिना मान को सुरक्षित रूप से परिवर्तित करने के लिए Number(value) का उपयोग करना सही पैटर्न है। यदि आप इसे (value: number) के रूप में एनोटेट करते हैं, तो रीचार्ट्स के आंतरिक हस्ताक्षर में यूनियन प्रकार के कारण टाइपस्क्रिप्ट में त्रुटि हो सकती है। टाइपस्क्रिप्ट को प्रकार का अनुमान लगाने दें और Number(value) के साथ कनवर्ट करें।
मैं रिचार्ज चार्ट को डार्क मोड में कैसे काम करवाऊं?
हार्डकोडेड हेक्स मानों के बजाय सभी रंगों के लिए सीएसएस कस्टम गुणों (चर) का उपयोग करें। ग्रिड लाइनों के लिए उन्हें hsl(var(--border)), अक्ष पाठ के लिए hsl(var(--muted-foreground)) और टूलटिप पृष्ठभूमि के लिए hsl(var(--background)) के साथ संदर्भित करें। ये वेरिएबल आपके टेलविंड थीम में परिभाषित हैं और स्वचालित रूप से <html> पर dark क्लास के माध्यम से प्रकाश और अंधेरे मोड के बीच मान स्विच करते हैं। कभी भी हार्डकोड #374151 न करें - यह थीम के साथ अपडेट नहीं होगा।
मैं चार्ट के लिए खाली या लोडिंग स्थितियों को कैसे संभालूं?
लोड करने के लिए: चार्ट को एक स्केलेटन (animate-pulse bg-muted rounded-lg h-80) से बदलें। खाली डेटा के लिए: ResponsiveContainer के अंदर एक केंद्रित खाली स्थिति संदेश प्रस्तुत करें। आंशिक डेटा के लिए: रिचार्ज विरल डेटा को खूबसूरती से संभालता है - बस आपके पास जो है उसे पास करें और यह बिंदुओं को जोड़ता है (या connectNulls={false} के साथ शून्य मानों के लिए अंतराल दिखाता है)।
क्या मुझे रीचार्ट्स या चार्ट.जेएस या डी3 का उपयोग करना चाहिए?
रिएक्ट-नेटिव डैशबोर्ड के लिए रिचार्ज - यह घोषणात्मक, संयोजन योग्य है, और रिएक्ट के रेंडरिंग मॉडल के साथ स्वाभाविक रूप से एकीकृत होता है। सरल उपयोग के मामलों के लिए चार्ट.जेएस जहां आप कैनवास-आधारित रेंडरर चाहते हैं (10,000+ डेटा बिंदुओं के लिए बेहतर प्रदर्शन)। डी3 जब आपको कस्टम, गैर-मानक चार्ट प्रकारों की आवश्यकता होती है जिन्हें कोई लाइब्रेरी कवर नहीं करती है - तो काफी अधिक कोड की अपेक्षा करें। एक एप्लिकेशन में एकाधिक चार्टिंग लाइब्रेरीज़ को मिश्रित करने से बचें।
मैं रिपोर्ट के लिए छवियों के रूप में चार्ट कैसे निर्यात करूं?
चार्ट कंटेनर को एक छवि के रूप में कैप्चर करने के लिए html2canvas लाइब्रेरी का उपयोग करें: const canvas = await html2canvas(chartRef.current); const url = canvas.toDataURL('image/png');। वैकल्पिक रूप से, Recharts के अंतर्निहित SVG आउटपुट का उपयोग करें - चूंकि Recharts SVG प्रस्तुत करता है, आप वेक्टर निर्यात के लिए सीधे SVG तत्व को क्रमबद्ध कर सकते हैं। पीडीएफ रिपोर्ट के लिए, पपेटियर या हेडलेस ब्राउज़र का उपयोग करके चार्ट सर्वर-साइड प्रस्तुत करें।
अगले चरण
डेटा विज़ुअलाइज़ेशन कच्चे मेट्रिक्स को व्यावसायिक बुद्धिमत्ता में बदल देता है। इस गाइड में पैटर्न - उत्तरदायी कंटेनर, कस्टम टूलटिप्स, रीयल-टाइम स्ट्रीमिंग और थीम-जागरूक रंग - आपको डैशबोर्ड यूआई के लिए आधार प्रदान करते हैं जो किसी भी डिवाइस और किसी भी रंग योजना में स्पष्ट रूप से संचार करते हैं।
ECOSIRE वित्तीय, परिचालन और प्रदर्शन मेट्रिक्स के लिए 8 डैशबोर्ड मॉड्यूल में रिचार्ज के साथ एनालिटिक्स डैशबोर्ड बनाता है - NestJS API और Drizzle-संचालित PostgreSQL के लाइव डेटा के साथ। बिजनेस इंटेलिजेंस और पावर बीआई कार्यान्वयन के लिए, हमारी पावर बीआई सेवाओं का पता लगाएं या हमारी सभी सेवाएं देखें।
लेखक
ECOSIRE Research and Development Team
ECOSIRE में एंटरप्राइज़-ग्रेड डिजिटल उत्पाद बना रहे हैं। Odoo एकीकरण, ई-कॉमर्स ऑटोमेशन, और AI-संचालित व्यावसायिक समाधानों पर अंतर्दृष्टि साझा कर रहे हैं।
संबंधित लेख
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.