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 الفاظ|

ریچارٹس کے ساتھ ڈیٹا ویژولائزیشن: ڈیش بورڈ پیٹرنز

ایک زبردست ڈیش بورڈ صرف ایک ٹیبل میں نمبر نہیں ہوتا ہے - یہ بصری نمونے ہیں جو رجحانات، بے ضابطگیوں اور مواقع کو ایک نظر میں ظاہر کرتے ہیں۔ Recharts React ایکو سسٹم میں سب سے زیادہ استعمال ہونے والی چارٹنگ لائبریری ہے: D3.js بنیادی اصولوں پر بنائی گئی، مکمل طور پر TypeScript ٹائپ، کمپوز ایبل، اور باکس سے باہر ریسپانسیو۔ یہ SVG پیچیدگی کو سنبھالتا ہے تاکہ آپ ڈیٹا کی کہانی پر توجہ مرکوز کرسکیں۔

یہ گائیڈ پروڈکشن ڈیش بورڈز کے لیے ریچارٹس پیٹرن کا احاطہ کرتی ہے — آمدنی کے رجحانات، تبادلوں کے فنلز، ٹائم سیریز میٹرکس، جغرافیائی تقسیم، اور ریئل ٹائم اسٹریمنگ ڈیٹا — ڈارک موڈ سپورٹ، قابل رسائی رنگ پیلیٹس، اور ریسپانسیو لے آؤٹس کے ساتھ۔

اہم ٹیک ویز

  • چارٹس کو ہمیشہ ResponsiveContainer میں width="100%" اور ایک مقررہ height کے ساتھ لپیٹیں — کبھی بھی پکسل کی چوڑائی کو ہارڈ کوڈ نہ کریں۔
  • پہلے سے طے شدہ کے بجائے CustomTooltip اجزاء استعمال کریں - فارمیٹنگ اور اسٹائلنگ پر مکمل کنٹرول
  • ریچارٹس formatter کال بیک پیرامیٹرز کو کبھی بھی واضح طور پر ٹائپ نہیں کیا جانا چاہئے — تبدیل کرنے کے لئے Number(value) استعمال کریں
  • ڈارک موڈ کو متحرک رنگ کی قدروں کی ضرورت ہوتی ہے - CSS متغیرات سے پڑھیں، ہارڈ کوڈڈ ہیکس رنگوں سے نہیں۔
  • ٹائم سیریز ڈیٹا کے لیے، XAxis پر ایک tickFormatter استعمال کریں تاکہ ٹائم اسٹیمپ کو مستقل طور پر فارمیٹ کریں۔
  • قابل رسائی: چارٹ کنٹینر میں aria-label شامل کریں اور لیجنڈ آئٹمز پر tabIndex استعمال کریں
  • کارکردگی: useMemo کے ساتھ چارٹ ڈیٹا کو میموائز کریں — ہر پیرنٹ رینڈر پر ری چارٹس دوبارہ رینڈر کرتا ہے۔
  • ریئل ٹائم ڈیٹا کے لیے، ڈیٹا اری ریفرنس کو اپ ڈیٹ کریں — ریچارٹس حوالہ کے لحاظ سے تبدیلیوں کا پتہ لگاتا ہے۔

سیٹ اپ

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

ریچارٹس 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 فارمیٹر کال بیکس کو اپنے پیرامیٹرز کو واضح طور پر کیوں نہیں ٹائپ کرنا چاہیے؟

formatter prop کے لیے Recharts کی TypeScript کی قسمیں ان طریقوں سے اوورلوڈ ہوتی ہیں جو آپ کے پیرامیٹرز کی تشریح کرتے وقت قسم کے تنازعات کا باعث بنتی ہیں۔ صحیح نمونہ یہ ہے کہ Number(value) کا استعمال value کی قسم کی تشریح کیے بغیر قدر کو محفوظ طریقے سے تبدیل کریں۔ اگر آپ اسے (value: number) کے بطور تشریح کرتے ہیں، تو ریچارٹس کے داخلی دستخط میں یونین کی قسم کی وجہ سے ٹائپ اسکرپٹ میں خرابی ہوسکتی ہے۔ TypeScript کو قسم کا اندازہ لگانے دیں اور Number(value) کے ساتھ تبدیل کریں۔

میں Recharts چارٹس کو ڈارک موڈ میں کیسے کام کروں؟

ہارڈ کوڈ شدہ ہیکس ویلیوز کے بجائے تمام رنگوں کے لیے CSS کسٹم پراپرٹیز (متغیرات) استعمال کریں۔ گرڈ لائنوں کے لیے hsl(var(--border))، محور متن کے لیے hsl(var(--muted-foreground))، اور ٹول ٹپ پس منظر کے لیے hsl(var(--background)) کے ساتھ ان کا حوالہ دیں۔ یہ متغیرات آپ کے Tailwind تھیم میں بیان کیے گئے ہیں اور <html> پر dark کلاس کے ذریعے لائٹ اور ڈارک موڈ کے درمیان اقدار کو خود بخود سوئچ کر دیتے ہیں۔ کبھی بھی ہارڈ کوڈ نہ کریں #374151 - یہ تھیم کے ساتھ اپ ڈیٹ نہیں ہوگا۔

میں چارٹس کے لیے خالی یا لوڈنگ اسٹیٹس کو کیسے ہینڈل کروں؟

لوڈنگ کے لیے: چارٹ کو سکیلیٹن (animate-pulse bg-muted rounded-lg h-80) سے تبدیل کریں۔ خالی ڈیٹا کے لیے: ایک ResponsiveContainer کے اندر مرکز میں خالی حالت کا پیغام بھیجیں۔ جزوی ڈیٹا کے لیے: ریچارٹس خوبصورتی سے ویرل ڈیٹا کو ہینڈل کرتا ہے — جو آپ کے پاس ہے اسے صرف پاس کریں اور یہ نقطوں کو جوڑتا ہے (یا connectNulls={false} کے ساتھ خالی اقدار کے لیے خلا دکھاتا ہے)۔

کیا مجھے Recharts یا Chart.js یا D3 استعمال کرنا چاہیے؟

ری ایکٹ کے مقامی ڈیش بورڈز کے لیے ریچارٹس — یہ اعلانیہ، کمپوز ایبل، اور قدرتی طور پر React کے رینڈرنگ ماڈل کے ساتھ مربوط ہوتا ہے۔ Chart.js آسان استعمال کے معاملات کے لیے جہاں آپ کینوس پر مبنی رینڈرر چاہتے ہیں (10,000+ ڈیٹا پوائنٹس کے لیے بہتر کارکردگی)۔ D3 جب آپ کو اپنی مرضی کے مطابق، غیر معیاری چارٹ کی قسموں کی ضرورت ہوتی ہے جن کا احاطہ کوئی لائبریری نہیں کرتا — نمایاں طور پر مزید کوڈ کی توقع کریں۔ ایک درخواست میں متعدد چارٹنگ لائبریریوں کو ملانے سے گریز کریں۔

میں رپورٹس کے لیے تصاویر کے بطور چارٹ کیسے برآمد کروں؟

چارٹ کنٹینر کو بطور تصویر کیپچر کرنے کے لیے html2canvas لائبریری کا استعمال کریں: const canvas = await html2canvas(chartRef.current); const url = canvas.toDataURL('image/png');۔ متبادل طور پر، Recharts کا بلٹ ان SVG آؤٹ پٹ استعمال کریں — چونکہ Recharts SVG کو رینڈر کرتا ہے، آپ ویکٹر ایکسپورٹ کے لیے براہ راست SVG عنصر کو سیریلائز کر سکتے ہیں۔ پی ڈی ایف رپورٹس کے لیے، Puppeteer یا ہیڈ لیس براؤزر کا استعمال کرتے ہوئے چارٹس سرور سائیڈ رینڈر کریں۔


اگلے اقدامات

ڈیٹا ویژولائزیشن خام میٹرکس کو کاروباری ذہانت میں بدل دیتی ہے۔ اس گائیڈ میں پیٹرن — ریسپانسیو کنٹینرز، حسب ضرورت ٹول ٹِپس، ریئل ٹائم اسٹریمنگ، اور تھیم سے آگاہ رنگ — آپ کو ڈیش بورڈ UIs کی بنیاد فراہم کرتے ہیں جو کسی بھی ڈیوائس پر اور کسی بھی رنگ سکیم میں واضح طور پر بات چیت کرتے ہیں۔

ECOSIRE مالی، آپریشنل، اور کارکردگی کے میٹرکس کے لیے 8 ڈیش بورڈ ماڈیولز پر ریچارٹس کے ساتھ تجزیاتی ڈیش بورڈز بناتا ہے — NestJS APIs اور Drizzle سے چلنے والے PostgreSQL کے لائیو ڈیٹا کے ساتھ۔ کاروباری ذہانت اور پاور BI کے نفاذ کے لیے، ہماری Power BI سروسز کو دریافت کریں یا ہماری تمام سروسز دیکھیں۔

E

تحریر

ECOSIRE Research and Development Team

ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔

Chat on WhatsApp