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 मार्च 202610 मिनट पढ़ें2.2k शब्द|

रिचार्ज के साथ डेटा विज़ुअलाइज़ेशन: डैशबोर्ड पैटर्न

एक महान डैशबोर्ड केवल तालिका में संख्याएँ नहीं है - यह दृश्य पैटर्न है जो एक नज़र में रुझान, विसंगतियों और अवसरों को प्रकट करता है। रिएक्ट इकोसिस्टम में रीचार्ट्स सबसे व्यापक रूप से उपयोग की जाने वाली चार्टिंग लाइब्रेरी है: 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 के लाइव डेटा के साथ। बिजनेस इंटेलिजेंस और पावर बीआई कार्यान्वयन के लिए, हमारी पावर बीआई सेवाओं का पता लगाएं या हमारी सभी सेवाएं देखें

शेयर करें:
E

लेखक

ECOSIRE Research and Development Team

ECOSIRE में एंटरप्राइज़-ग्रेड डिजिटल उत्पाद बना रहे हैं। Odoo एकीकरण, ई-कॉमर्स ऑटोमेशन, और AI-संचालित व्यावसायिक समाधानों पर अंतर्दृष्टि साझा कर रहे हैं।

WhatsApp पर चैट करें