کارت متریک (MetricCard)

کامپوننت نمایش معیارها و آمار به صورت خلاصه با پشتیبانی از نمودار و درصد تغییر

معرفی

کامپوننت Metric Card برای نمایش معیارها و آمارهای مهم به صورت خلاصه استفاده می‌شود. این کامپوننت شامل عنوان، مقدار، درصد تغییر و نمودار کوچک (Sparkline) است.

چه زمانی استفاده کنیم:

  • برای نمایش KPIهای اصلی در داشبورد (بازدید، کاربران فعال، درآمد)
  • وقتی نیاز به نمایش روند تغییرات با sparkline دارید
  • برای متریک‌هایی که درصد تغییر نسبت به دوره قبل مهم است

چه زمانی استفاده نکنیم:

  • برای نمایش یک عدد ساده بدون sparkline — از StatDisplay استفاده کنید
  • برای محتوای عمومی غیرعددی — از Card استفاده کنید

استفاده پایه

کاربران فعال

4,605
کاهش-0.1٪نسبت به 7 روز قبل
  • 4 دی: 4,000
  • 4 دی: 4,150
  • 4 دی: 4,100
  • 4 دی: 4,250
  • 4 دی: 4,300
  • 4 دی: 4,400
  • 4 دی: 4,350
  • 4 دی: 4,500
  • 4 دی: 4,550
  • 4 دی: 4,600
  • 4 دی: 4,580
  • 4 دی: 4,605

زمین بازی

با تغییر تنظیمات زیر، کارت متریک را به صورت زنده مشاهده کنید.

زمین بازی

بازدید کل

12,456
افزایش+12.5٪
تنظیمات
محتوا
داده
ظاهر
کد این نمونه به‌صورت خودکار قابل تولید نیست — برای کد آماده‌ی copy/paste به بخش «استفاده» در بالای صفحه مراجعه کنید.
import {
  MetricCard,
  MetricCardHeader,
  MetricCardLabel,
  MetricCardContent,
  MetricCardValue,
  MetricCardDifferential,
  MetricCardSparkline,
} from '@partodata/ui'
import { Users } from 'lucide-react'

export default function MyComponent() {
  const data = [
    { value: 4000, timestamp: 1735689600 },
    { value: 4200, timestamp: 1735776000 },
    { value: 4100, timestamp: 1735862400 },
    // ...
  ]

  return (
    <MetricCard>
      <MetricCardHeader>
        <MetricCardLabel icon={<Users className="h-3.5 w-3.5" />}>کاربران فعال</MetricCardLabel>
      </MetricCardHeader>
      <MetricCardContent>
        <MetricCardValue>4,605</MetricCardValue>
        <MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="down" tone="negative">
          -0.1%
        </MetricCardDifferential>
      </MetricCardContent>
      <MetricCardSparkline data={data} dataKey="value" />
    </MetricCard>
  )
}

حالت‌های مختلف

با آیکونِ رنگی (MetricCardIcon)

MetricCardIcon یک باکسِ رنگی برای آیکون است — برای زمانی که آیکون فقط یک نشانهٔ کوچکِ کنارِ عنوان نیست، بلکه خودش بخشی از هویتِ بصریِ کارت است (مثلاً یک ردیف از KPIها که هرکدام با یک رنگِ معنایی جدا می‌شوند). رنگ از یک مجموعهٔ ثابتِ معنایی می‌آید — همان مجموعه‌ای که Callout استفاده می‌کند، به‌علاوهٔ brand — نه هر رنگِ دلخواه:

کاربران فعال

4,605
افزایش+2.5٪نسبت به 7 روز قبل
import {
  MetricCard,
  MetricCardIcon,
  MetricCardLabel,
  MetricCardContent,
  MetricCardValue,
  MetricCardDifferential,
  formatNumber,
} from '@partodata/ui'
import { Users } from 'lucide-react'
;<MetricCard className="flex flex-row items-center gap-3 px-4 py-3">
  <MetricCardIcon variant="brand">
    <Users />
  </MetricCardIcon>
  <div className="min-w-0 flex-1">
    <MetricCardLabel>کاربران فعال</MetricCardLabel>
    <MetricCardContent className="px-0">
      <MetricCardValue>{formatNumber(4605)}</MetricCardValue>
      <MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="up" tone="positive">
        +2.5%
      </MetricCardDifferential>
    </MetricCardContent>
  </div>
</MetricCard>

شش مقدارِ variant در دسترس است: brand، success، warning، destructive، info، neutral (پیش‌فرض). سه اندازه هم دارد: sm / md (پیش‌فرض) / lg. آیکونِ فرزند خودکار با اندازهٔ باکس هم‌خوان می‌شود (نیازی به تنظیمِ دستیِ className روی خودِ آیکون نیست).

اگر رنگِ خارج از این مجموعه لازم دارید

MetricCardIcon عمداً بسته است — یک رنگِ اختصاصیِ محصول (مثلاً هم‌رنگ‌کردن با یک پالتِ status-dot مختص به یک پنل خاص) به این معنا نیست که باید یک مقدارِ جدید به variant اضافه شود. className را مستقیماً روی MetricCardIcon بدهید تا رنگِ پس‌زمینه/متن را override کند.

با آیکون و Tooltip

4,360
کاهش-4.3٪نسبت به 7 روز قبل
  • 4 دی: 4,200
  • 4 دی: 4,280
  • 4 دی: 4,250
  • 4 دی: 4,320
  • 4 دی: 4,300
  • 4 دی: 4,380
  • 4 دی: 4,350
  • 4 دی: 4,400
  • 4 دی: 4,320
  • 4 دی: 4,360
  • 4 دی: 4,340
  • 4 دی: 4,360
import { Users } from 'lucide-react'
;<MetricCard>
  <MetricCardHeader>
    <MetricCardLabel icon={<Users className="h-3.5 w-3.5" />} tooltip="تعداد کاربران فعال در 24 ساعت گذشته">
      کاربران فعال
    </MetricCardLabel>
  </MetricCardHeader>
  <MetricCardContent>
    <MetricCardValue>4,605</MetricCardValue>
    <MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="up" tone="positive">
      +2.5%
    </MetricCardDifferential>
  </MetricCardContent>
  <MetricCardSparkline data={data} dataKey="value" />
</MetricCard>

با لینک خارجی

درآمد ماهانه

باز کردن لینک
250 میلیون
افزایش+12.3٪نسبت به 7 روز قبل
  • 4 دی: 150,000
  • 4 دی: 155,000
  • 4 دی: 158,000
  • 4 دی: 165,000
  • 4 دی: 170,000
  • 4 دی: 180,000
  • 4 دی: 185,000
  • 4 دی: 195,000
  • 4 دی: 210,000
  • 4 دی: 225,000
  • 4 دی: 240,000
  • 4 دی: 250,000
<MetricCard>
  <MetricCardHeader href="https://example.com/analytics">
    <MetricCardLabel>درآمد ماهانه</MetricCardLabel>
  </MetricCardHeader>
  <MetricCardContent>
    <MetricCardValue>250 میلیون</MetricCardValue>
    <MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="up" tone="positive">
      +12.3%
    </MetricCardDifferential>
  </MetricCardContent>
  <MetricCardSparkline data={revenueData} dataKey="value" />
</MetricCard>

کارت پیوندی (href)

شاخصی که مقصد دارد («21 مسئلهٔ نیازمند توجه» ← فهرست آن مسائل) href می‌گیرد و کل کارت یک پیوند می‌شود: پیوند کشیده، حلقهٔ فوکوس دور کارت، hover:border-strong. پیوند با linkComponent قاب ساخته می‌شود (پیوند روتر، با کلیک وسط در تب تازه)، target ندارد، و tooltip برچسب، پیوند header و نمودار روی آن قابل‌استفاده می‌مانند.

21
افزایش24٪نسبت به 7 روز قبل
  • 4 دی: 14
  • 4 دی: 15
  • 4 دی: 13
  • 4 دی: 17
  • 4 دی: 18
  • 4 دی: 21

خطای جمع‌آوری

3
کاهش40٪نسبت به 7 روز قبل
<MetricCard href="/issues?level=urgent,high" aria-label="مسائل نیازمند توجه: 21، باز کردن فهرست">
  <MetricCardHeader>
    <MetricCardLabel tooltip="مسائلی که شدت بالا یا فوری دارند">نیازمند توجه</MetricCardLabel>
  </MetricCardHeader>
  <MetricCardContent>
    <MetricCardValue>21</MetricCardValue>
  </MetricCardContent>
</MetricCard>

قاعده: شمار روی کارت با شمار فهرست مقصد برابر باشد (کارتی که 21 می‌گوید فهرستی با 21 مورد باز می‌کند). نام پیوند aria-label کارت است (مقصد را در آن بنویسید) و بدون آن برچسب و مقدار؛ بدون هر دو در توسعه هشدار می‌دهد. aria-label روی div کارت نمی‌ماند، چون نام روی عنصر بی‌نقش ممنوع است و به پیوند می‌رود.

بدون نمودار

کاربران جدید

1,234
افزایش+5.2٪نسبت به 7 روز قبل
<MetricCard>
  <MetricCardHeader>
    <MetricCardLabel>کاربران جدید</MetricCardLabel>
  </MetricCardHeader>
  <MetricCardContent>
    <MetricCardValue>1,234</MetricCardValue>
    <MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="up" tone="positive">
      +5.2%
    </MetricCardDifferential>
  </MetricCardContent>
</MetricCard>

حالت بارگذاری

<MetricCard isLoading={true} />

حالت انگلیسی (LTR)

4,870
افزایش+36.0%نسبت به 7 روز قبل
  • Dec 24: 4,500
  • Dec 24: 4,590
  • Dec 24: 4,680
  • Dec 24: 4,720
  • Dec 24: 4,760
  • Dec 24: 4,800
  • Dec 24: 4,820
  • Dec 24: 4,850
  • Dec 24: 4,870
  • Dec 24: 4,900
  • Dec 24: 4,920
  • Dec 24: 4,870
<MetricCard dir="ltr">
  <MetricCardHeader href="https://example.com">
    <MetricCardLabel tooltip="Number of active users in the last 24 hours">Active Users</MetricCardLabel>
  </MetricCardHeader>
  <MetricCardContent>
    <MetricCardValue>4,605</MetricCardValue>
    <MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="down" tone="negative">
      -0.1%
    </MetricCardDifferential>
  </MetricCardContent>
  <MetricCardSparkline data={data} dataKey="value" />
</MetricCard>

مقایسه با دورهٔ قبل

previous روی MetricCardValue مقدار دورهٔ قبل را کمرنگ کنار مقدار فعلی می‌گذارد، هر کدام با برچسبش. تغییر را با TrendIndicator در ردیف بعد نشان دهید.

<MetricCard>
  <MetricCardHeader>
    <MetricCardLabel>بازدید</MetricCardLabel>
  </MetricCardHeader>
  <MetricCardContent>
    <MetricCardValue currentLabel="این ماه" previous={{ value: '980', label: 'ماه قبل' }}>
      1,240
    </MetricCardValue>
  </MetricCardContent>
  <MetricCardContent>
    <TrendIndicator comparisonLabel="نسبت به 7 روز قبل" value={26.5} showPercent size="sm" />
  </MetricCardContent>
</MetricCard>

Props

MetricCard

Prop

Type

MetricCardHeader

Prop

Type

MetricCardIcon

Prop

Type

MetricCardLabel

Prop

Type

MetricCardDifferential

Prop

Type

MetricCardSparkline

Prop

Type

مثال‌های کاربردی

داشبورد آنالیتیکس

const metrics = [
  {
    label: 'کاربران فعال',
    value: '4,605',
    differential: '-0.1%',
    direction: 'down',
    tone: 'negative',
    data: activeUsersData,
  },
  {
    label: 'فروش امروز',
    value: '123 میلیون',
    differential: '+5.2%',
    direction: 'up',
    tone: 'positive',
    data: salesData,
  },
  {
    label: 'نرخ تبدیل',
    value: '3.24%',
    differential: '+0.8%',
    direction: 'up',
    tone: 'positive',
    data: conversionData,
  },
]

;<div className="grid grid-cols-1 md:grid-cols-3 gap-4" dir="rtl">
  {metrics.map((metric) => (
    <MetricCard key={metric.label}>
      <MetricCardHeader>
        <MetricCardLabel>{metric.label}</MetricCardLabel>
      </MetricCardHeader>
      <MetricCardContent>
        <MetricCardValue>{metric.value}</MetricCardValue>
        <MetricCardDifferential
          comparisonLabel="نسبت به 1 تا 30 شهریور"
          direction={metric.direction}
          tone={metric.tone}
        >
          {metric.differential}
        </MetricCardDifferential>
      </MetricCardContent>
      <MetricCardSparkline data={metric.data} dataKey="value" />
    </MetricCard>
  ))}
</div>

با React Query

پاسخ fetchMetrics باید نام دورهٔ واقعی مقایسه را در comparisonLabel برگرداند؛ بدون درصدِ قابل محاسبه و این برچسب، تغییر نمایش داده نمی‌شود.

import { useQuery } from '@tanstack/react-query'

function MetricCardWithQuery() {
  const { data, isLoading } = useQuery({
    queryKey: ['metrics'],
    queryFn: fetchMetrics,
  })

  return (
    <MetricCard isLoading={isLoading}>
      <MetricCardHeader>
        <MetricCardLabel>کاربران فعال</MetricCardLabel>
      </MetricCardHeader>
      <MetricCardContent>
        <MetricCardValue>{data?.value.toLocaleString('en-US')}</MetricCardValue>
        {typeof data?.trend === 'number' && data.comparisonLabel?.trim() && (
          <MetricCardDifferential
            comparisonLabel={data.comparisonLabel}
            direction={data.trend > 0 ? 'up' : data.trend < 0 ? 'down' : 'neutral'}
            tone={data.trend > 0 ? 'positive' : data.trend < 0 ? 'negative' : 'neutral'}
          >
            {data.trend > 0 ? '+' : ''}
            {data.trend}%
          </MetricCardDifferential>
        )}
      </MetricCardContent>
      <MetricCardSparkline data={data?.history || []} dataKey="value" />
    </MetricCard>
  )
}

نکات مهم

ساختار داده Sparkline

نمودار Sparkline نیاز به آرایه‌ای از داده دارد که هر آیتم شامل:

type SparklineData = {
  value: number // مقدار عددی
  timestamp: number // Unix seconds؛ مثلا 1735689600
}

جهت و رنگ Differential: دو محور

از 7.9 پیکان و رنگ دو محور جدا هستند، همان قرارداد TrendIndicator:

  • direction (up · down · neutral): پیکان و واژهٔ صفحه‌خوان. از علامت عدد می‌آید.
  • tone (positive · negative · neutral): رنگ. از قطبیت شاخص می‌آید: افزایش نرخ تعامل positive، افزایش نرخ خطا negative، و افزایش حجم منشن‌ها neutral (حجم قطبیت ندارد: بیشتر بودن آن نه خبر خوب است نه بد).
<MetricCardDifferential direction="up" tone="neutral" comparisonLabel="نسبت به 7 روز قبل">24٪</MetricCardDifferential>
<MetricCardDifferential direction="down" tone="positive" comparisonLabel="نسبت به ماه قبل">8٪</MetricCardDifferential>

variant="positive" / "negative" که هر دو را یکی می‌کرد منسوخ است. بدون هیچ‌کدام از سه prop، همان رفتار قبل (یک افزایش مثبت) می‌ماند.

رنگ هرگز تنها حاملِ معنا نیست: پیکان جهت‌دار هم رندر می‌شود و واژهٔ جهت («افزایش» / «کاهش») به‌صورت متن پنهان برای صفحه‌خوان‌ها منتشر می‌شود. اگر متنِ مقدار خودش جهت را می‌گوید و فلش را اضافی می‌دانید، showIcon={false} بدهید — ولی بدانید که در آن حالت تنها تفاوت دیداریِ مثبت و منفی رنگ است.

نمایش علامت مثبت/منفی

محتوای MetricCardDifferential داخل یک span با dir="ltr" رندر می‌شود؛ بنابراین علامتی که در ابتدای مقدار تایپ می‌کنید (مثل + یا -) در همان ترتیب و سمت درستِ عدد باقی می‌ماند، حتی داخل کارت RTL. علامت را همان‌جا که می‌خواهید در ابتدای children بگذارید:

<MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="down" tone="negative">-0.1٪</MetricCardDifferential>
<MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="up" tone="positive">+12٪</MetricCardDifferential>

اگر می‌خواهید علامت به‌صورت جدا و بعد از مقدار افزوده شود، از prop sign استفاده کنید (کامپوننت علامت را به‌طور خودکار درج یا جابه‌جا نمی‌کند؛ children دقیقاً همان‌گونه که پاس می‌دهید نمایش داده می‌شود):

<MetricCardDifferential comparisonLabel="نسبت به 1 تا 30 شهریور" direction="down" tone="negative" sign="-">
  0.1٪
</MetricCardDifferential>

RTL Support

کامپوننت به طور کامل از RTL پشتیبانی می‌کند. برای استفاده در محیط انگلیسی، dir="ltr" را به MetricCard اضافه کنید:

<MetricCard dir="ltr">{/* ... */}</MetricCard>

وزن مقدار

مقدار (MetricCardValue) نقش عدد شاخص را دارد: .text-stat، 24 / 32 با وزن 400 (tabular-nums داخل آن در این فونت بی‌اثر است)، نه پررنگ و نه بلندتر از عنوان صفحه (22) بیش از یک پله. این کامپوننت همچنان KPIِ تیتر است، همراه sparkline و differential اختیاری. برای عدد آرام و بدون قاب از InlineStat استفاده کنید.

راهنمای استفاده

بکنید

  • همیشه MetricCardLabel با tooltip توضیحی همراه کنید تا کاربر متریک را درک کند - برای داده‌های async از prop isLoading استفاده کنید تا layout shift نداشته باشید - در grid layout از تعداد یکسان MetricCard در هر ردیف استفاده کنید

نکنید

  • بیش از 4 MetricCard در یک ردیف قرار ندهید — خوانایی کاهش می‌یابد - مقادیر را بدون واحد نمایش ندهید — کاربر باید بداند عدد چه معنایی دارد - از variant="negative" برای مقادیر مثبت استفاده نکنید — رنگ باید با معنا هم‌خوانی داشته باشد

دسترسی‌پذیری

  • MetricCardLabel با tooltip یک دکمهٔ واقعی رندر می‌کند؛ بنابراین تعریف متریک با Tab قابل رسیدن است و با فوکوس (بدون ماوس) باز می‌شود. تیتر h3 و ترتیب عنوان‌ها دست‌نخورده می‌ماند
  • حالت بارگذاری از Skeleton استفاده می‌کند و layout shift را کاهش می‌دهد
  • جهت differential با دو کانال منتقل می‌شود: فلش جهت‌دار (شکل) و رنگ. واژهٔ جهت («افزایش» / «کاهش») هم به‌صورت sr-only خوانده می‌شود. اگر با showIcon={false} فلش را خاموش کنید، مسئولیت کانال دوم — مثلاً علامت +/- در children یا prop sign — با شماست
  • MetricCardSparkline یک ایستگاه Tab است: با فوکوس، اولین نقطه انتخاب می‌شود و کلیدهای ← و → مکان‌نما را روی نقطه‌ها می‌برند (Home/End برای ابتدا و انتها، Escape برای بستن). نمودار در RTL آینه نمی‌شود، پس → همیشه نقطهٔ سمت راست (متأخرتر) است
  • همان تاریخ و مقداری که در tooltipِ نمودار دیده می‌شود، به‌صورت فهرست پنهان (sr-only) هم منتشر می‌شود؛ برچسب نمودار از locale ساخته می‌شود («نمودار روند از … تا …») و دیگر در همهٔ زبان‌ها فارسیِ ثابت نیست
  • کارت پیوندی یک پیوند واقعی (a) با حلقهٔ فوکوس دور کل کارت است؛ نامش aria-label کارت یا برچسب و مقدار است. tooltip برچسب و نمودار بالای پیوند می‌مانند و با صفحه‌کلید در دسترس‌اند (ترتیب Tab: دکمهٔ tooltip، سپس پیوند)
  • لینک header نام دسترس‌پذیر خود را از یک متن پنهان (sr-only) با محتوای «باز کردن لینک» می‌گیرد

کامپوننت‌های مرتبط

  • Card — اگر محتوای شما عمومی است و نیاز به sparkline ندارید، از Card استفاده کنید
  • TrendIndicator — اگر عدد رشد بیرون از MetricCard می‌آید (جدول، ردیف)؛ همان دو محور direction و tone
  • StatDisplay — اگر فقط یک عدد با label نیاز دارید بدون sparkline و differential، از StatDisplay استفاده کنید

دورهٔ رشد

comparisonLabel?: string دورهٔ واقعی مقایسه را مستقیم کنار عدد نشان می‌دهد. در مثال‌های این صفحه، دادهٔ نمایشی نسبت به 1 تا 30 شهریور تعریف شده است. در محصول، برچسب را از داده یا درخواست مقایسه بگیرید؛ دورهٔ نامعلوم را با «دورهٔ قبل» جایگزین نکنید. نبود برچسب در توسعه هشدار می‌دهد و مقدار قدیمی را پنهان نمی‌کند.